chore(git): merge pull request #97 from feature/profile-page into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 47s
All checks were successful
Release to Staging / build_and_release (push) Successful in 47s
Reviewed-on: #97
This commit is contained in:
commit
9b8bf01d33
31
index.html
31
index.html
@ -9,6 +9,37 @@
|
||||
<link rel="stylesheet" href="/assets/fonts/fontawesome-all.min.css" />
|
||||
|
||||
<title>DEG Mods - Liberating Game Mods</title>
|
||||
|
||||
<!-- Start Single Page Apps for GitHub Pages -->
|
||||
<script type="text/javascript">
|
||||
// Single Page Apps for GitHub Pages
|
||||
// MIT License
|
||||
// https://github.com/rafgraph/spa-github-pages
|
||||
// This script checks to see if a redirect is present in the query string,
|
||||
// converts it back into the correct url and adds it to the
|
||||
// browser's history using window.history.replaceState(...),
|
||||
// which won't cause the browser to attempt to load the new url.
|
||||
// When the single page app is loaded further down in this file,
|
||||
// the correct url will be waiting in the browser's history for
|
||||
// the single page app to route accordingly.
|
||||
;(function (l) {
|
||||
if (l.search[1] === '/') {
|
||||
var decoded = l.search
|
||||
.slice(1)
|
||||
.split('&')
|
||||
.map(function (s) {
|
||||
return s.replace(/~and~/g, '&')
|
||||
})
|
||||
.join('?')
|
||||
window.history.replaceState(
|
||||
null,
|
||||
null,
|
||||
l.pathname.slice(0, -1) + decoded + l.hash
|
||||
)
|
||||
}
|
||||
})(window.location)
|
||||
</script>
|
||||
<!-- End Single Page Apps for GitHub Pages -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
51
public/404.html
Normal file
51
public/404.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Single Page Apps for GitHub Pages</title>
|
||||
<script type="text/javascript">
|
||||
// Single Page Apps for GitHub Pages
|
||||
// MIT License
|
||||
// https://github.com/rafgraph/spa-github-pages
|
||||
// This script takes the current url and converts the path and query
|
||||
// string into just a query string, and then redirects the browser
|
||||
// to the new url with only a query string and hash fragment,
|
||||
// e.g. https://www.foo.tld/one/two?a=b&c=d#qwe, becomes
|
||||
// https://www.foo.tld/?/one/two&a=b~and~c=d#qwe
|
||||
// Note: this 404.html file must be at least 512 bytes for it to work
|
||||
// with Internet Explorer (it is currently > 512 bytes)
|
||||
|
||||
// If you're creating a Project Pages site and NOT using a custom domain,
|
||||
// then set pathSegmentsToKeep to 1 (enterprise users may need to set it to > 1).
|
||||
// This way the code will only replace the route part of the path, and not
|
||||
// the real directory in which the app resides, for example:
|
||||
// https://username.github.io/repo-name/one/two?a=b&c=d#qwe becomes
|
||||
// https://username.github.io/repo-name/?/one/two&a=b~and~c=d#qwe
|
||||
// Otherwise, leave pathSegmentsToKeep as 0.
|
||||
var pathSegmentsToKeep = 0
|
||||
|
||||
var l = window.location
|
||||
l.replace(
|
||||
l.protocol +
|
||||
'//' +
|
||||
l.hostname +
|
||||
(l.port ? ':' + l.port : '') +
|
||||
l.pathname
|
||||
.split('/')
|
||||
.slice(0, 1 + pathSegmentsToKeep)
|
||||
.join('/') +
|
||||
'/?/' +
|
||||
l.pathname
|
||||
.slice(1)
|
||||
.split('/')
|
||||
.slice(pathSegmentsToKeep)
|
||||
.join('/')
|
||||
.replace(/&/g, '~and~') +
|
||||
(l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
|
||||
l.hash
|
||||
)
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
19
src/App.tsx
19
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>
|
||||
<Route element={<Layout />}>
|
||||
{routes.map((route, index) => (
|
||||
<Route
|
||||
key={route.path + index}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Route>
|
||||
</Routes>
|
||||
)
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
||||
export default App
|
||||
|
@ -89,13 +89,27 @@ interface CheckboxFieldProps {
|
||||
name: string
|
||||
isChecked: boolean
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
type?: 'default' | 'stylized'
|
||||
}
|
||||
|
||||
export const CheckboxField = React.memo(
|
||||
({ label, name, isChecked, handleChange }: CheckboxFieldProps) => (
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||
<label className='form-label labelMain'>{label}</label>
|
||||
({
|
||||
label,
|
||||
name,
|
||||
isChecked,
|
||||
handleChange,
|
||||
type = 'default'
|
||||
}: CheckboxFieldProps) => (
|
||||
<div
|
||||
className={`inputLabelWrapperMain inputLabelWrapperMainAlt${
|
||||
type === 'stylized' ? ` inputLabelWrapperMainAltStylized` : ''
|
||||
}`}
|
||||
>
|
||||
<label htmlFor={name} className='form-label labelMain'>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
id={name}
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name={name}
|
||||
|
@ -395,6 +395,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
||||
name='nsfw'
|
||||
isChecked={formState.nsfw}
|
||||
handleChange={handleCheckboxChange}
|
||||
type='stylized'
|
||||
/>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<div className='labelWrapperMain'>
|
||||
|
@ -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 (
|
||||
|
26
src/components/Tabs.tsx
Normal file
26
src/components/Tabs.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
interface TabsProps {
|
||||
tabs: string[]
|
||||
tab: number
|
||||
setTab: React.Dispatch<React.SetStateAction<number>>
|
||||
}
|
||||
|
||||
export const Tabs = ({ tabs, tab, setTab }: TabsProps) => {
|
||||
return (
|
||||
<div className='IBMSMSplitMainFullSideSec IBMSMSMFSSNav'>
|
||||
{tabs.map((t, i) => {
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
className={`btn btnMain IBMSMSMFSSNavBtn${
|
||||
tab === i ? ' IBMSMSMFSSNavBtnActive' : ''
|
||||
}`}
|
||||
type='button'
|
||||
onClick={() => setTab(i)}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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<ModDetails[]> => {
|
||||
// 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
|
||||
|
@ -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
|
||||
|
10
src/layout/feed.tsx
Normal file
10
src/layout/feed.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
export const FeedLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<h1>WIP</h1>
|
||||
<Outlet />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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<boolean>(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 (
|
||||
<div
|
||||
className='socialNav'
|
||||
@ -32,30 +25,27 @@ export const SocialNav = () => {
|
||||
<div className='socialNavInside'>
|
||||
<NavButton
|
||||
to={appRoutes.home}
|
||||
isActive={isOnHomePage}
|
||||
viewBox="0 -32 576 576"
|
||||
viewBox='0 -32 576 576'
|
||||
svgPath='M511.8 287.6L512.5 447.7C512.5 450.5 512.3 453.1 512 455.8V472C512 494.1 494.1 512 472 512H456C454.9 512 453.8 511.1 452.7 511.9C451.3 511.1 449.9 512 448.5 512H392C369.9 512 352 494.1 352 472V384C352 366.3 337.7 352 320 352H256C238.3 352 224 366.3 224 384V472C224 494.1 206.1 512 184 512H128.1C126.6 512 125.1 511.9 123.6 511.8C122.4 511.9 121.2 512 120 512H104C81.91 512 64 494.1 64 472V360C64 359.1 64.03 358.1 64.09 357.2V287.6H32.05C14.02 287.6 0 273.5 0 255.5C0 246.5 3.004 238.5 10.01 231.5L266.4 8.016C273.4 1.002 281.4 0 288.4 0C295.4 0 303.4 2.004 309.5 7.014L416 100.7V64C416 46.33 430.3 32 448 32H480C497.7 32 512 46.33 512 64V185L564.8 231.5C572.8 238.5 576.9 246.5 575.8 255.5C575.8 273.5 560.8 287.6 543.8 287.6L511.8 287.6z'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.home}
|
||||
isActive={false}
|
||||
to={appRoutes.feed}
|
||||
svgPath='M88 48C101.3 48 112 58.75 112 72V120C112 133.3 101.3 144 88 144H40C26.75 144 16 133.3 16 120V72C16 58.75 26.75 48 40 48H88zM480 64C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H192C174.3 128 160 113.7 160 96C160 78.33 174.3 64 192 64H480zM480 224C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H192C174.3 288 160 273.7 160 256C160 238.3 174.3 224 192 224H480zM480 384C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H192C174.3 448 160 433.7 160 416C160 398.3 174.3 384 192 384H480zM16 232C16 218.7 26.75 208 40 208H88C101.3 208 112 218.7 112 232V280C112 293.3 101.3 304 88 304H40C26.75 304 16 293.3 16 280V232zM88 368C101.3 368 112 378.7 112 392V440C112 453.3 101.3 464 88 464H40C26.75 464 16 453.3 16 440V392C16 378.7 26.75 368 40 368H88z'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.home}
|
||||
isActive={false}
|
||||
to={appRoutes.notifications}
|
||||
svgPath='M256 32V51.2C329 66.03 384 130.6 384 208V226.8C384 273.9 401.3 319.2 432.5 354.4L439.9 362.7C448.3 372.2 450.4 385.6 445.2 397.1C440 408.6 428.6 416 416 416H32C19.4 416 7.971 408.6 2.809 397.1C-2.353 385.6-.2883 372.2 8.084 362.7L15.5 354.4C46.74 319.2 64 273.9 64 226.8V208C64 130.6 118.1 66.03 192 51.2V32C192 14.33 206.3 0 224 0C241.7 0 256 14.33 256 32H256zM224 512C207 512 190.7 505.3 178.7 493.3C166.7 481.3 160 464.1 160 448H288C288 464.1 281.3 481.3 269.3 493.3C257.3 505.3 240.1 512 224 512z'
|
||||
/>
|
||||
<NavButton
|
||||
to={appRoutes.search}
|
||||
isActive={isOnSearchPage}
|
||||
svgPath='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'
|
||||
/>
|
||||
{!!userState.auth && (
|
||||
<NavButton
|
||||
to={getProfilePageRoute('xyz')}
|
||||
isActive={isOnProfilePage}
|
||||
to={getProfilePageRoute('')}
|
||||
svgPath='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className='socialNavCollapse' onClick={toggleNav}>
|
||||
@ -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) => (
|
||||
<Link
|
||||
to={to}
|
||||
className={`btn btnMain socialNavInsideBtn ${
|
||||
const NavButton = ({
|
||||
svgPath,
|
||||
viewBox = '0 0 512 512',
|
||||
...rest
|
||||
}: NavButtonProps) => (
|
||||
<NavLink
|
||||
{...rest}
|
||||
className={({ isActive }) =>
|
||||
`btn btnMain socialNavInsideBtn ${
|
||||
isActive ? 'socialNavInsideBtnActive' : ''
|
||||
}`}
|
||||
}`
|
||||
}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@ -101,7 +95,5 @@ const NavButton = ({ to, isActive, svgPath, viewBox = '0 0 512 512' }: NavButton
|
||||
>
|
||||
<path d={svgPath}></path>
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
|
||||
|
||||
</NavLink>
|
||||
)
|
||||
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
<NDKContextProvider>
|
||||
<App />
|
||||
</NDKContextProvider>
|
||||
<ToastContainer />
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
30
src/pages/404.tsx
Normal file
30
src/pages/404.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { appRoutes } from 'routes'
|
||||
|
||||
export const NotFoundPage = () => {
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>Page not found</h2>
|
||||
</div>
|
||||
<div>
|
||||
<p>The page you're attempting to visit doesn't exist</p>
|
||||
</div>
|
||||
<div className='IBMSMAction'>
|
||||
<Link
|
||||
to={appRoutes.home}
|
||||
className='btn btnMain IBMSMActionBtn'
|
||||
type='button'
|
||||
>
|
||||
Go home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
3
src/pages/feed.tsx
Normal file
3
src/pages/feed.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const FeedPage = () => {
|
||||
return <h1>Feed</h1>
|
||||
}
|
@ -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?
|
||||
</label>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>
|
||||
Actually CP
|
||||
</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.actuallyCP}
|
||||
onChange={() => handleCheckboxChange('actuallyCP')}
|
||||
{MOD_REPORT_REASONS.map((r) => (
|
||||
<CheckboxField
|
||||
key={r.key}
|
||||
label={r.label}
|
||||
name={r.key}
|
||||
isChecked={selectedOptions[r.key]}
|
||||
handleChange={() => handleCheckboxChange(r.key)}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>Spam</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.spam}
|
||||
onChange={() => handleCheckboxChange('spam')}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>Scam</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.scam}
|
||||
onChange={() => handleCheckboxChange('scam')}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>
|
||||
Not a game mod
|
||||
</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.notAGameMod}
|
||||
onChange={() => handleCheckboxChange('notAGameMod')}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>
|
||||
Stolen game mod
|
||||
</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.stolenGameMod}
|
||||
onChange={() => handleCheckboxChange('stolenGameMod')}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>
|
||||
Wasn't tagged NSFW
|
||||
</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.wasntTaggedNSFW}
|
||||
onChange={() => handleCheckboxChange('wasntTaggedNSFW')}
|
||||
/>
|
||||
</div>
|
||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
||||
<label className='form-label labelMain'>
|
||||
Other reason
|
||||
</label>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='CheckboxMain'
|
||||
name='reportOption'
|
||||
checked={selectedOptions.otherReason}
|
||||
onChange={() => handleCheckboxChange('otherReason')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className='btn btnMain pUMCB_Report'
|
||||
|
3
src/pages/notifications.tsx
Normal file
3
src/pages/notifications.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export const NotificationsPage = () => {
|
||||
return <h1>Notifications</h1>
|
||||
}
|
@ -1,3 +1,722 @@
|
||||
import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ModCard } from 'components/ModCard'
|
||||
import { ModFilter } from 'components/ModsFilter'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
import { ProfileSection } from 'components/ProfileSection'
|
||||
import { Tabs } from 'components/Tabs'
|
||||
import { MOD_FILTER_LIMIT } from '../constants'
|
||||
import {
|
||||
useAppSelector,
|
||||
useDidMount,
|
||||
useFilteredMods,
|
||||
useMuteLists,
|
||||
useNDKContext,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useParams, Navigate, Link } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
ModeratedFilter,
|
||||
NSFWFilter,
|
||||
SortBy,
|
||||
UserProfile,
|
||||
UserRelaysType
|
||||
} from 'types'
|
||||
import {
|
||||
copyTextToClipboard,
|
||||
now,
|
||||
npubToHex,
|
||||
scrollIntoView,
|
||||
sendDMUsingRandomKey,
|
||||
signAndPublish
|
||||
} from 'utils'
|
||||
import { CheckboxField } from 'components/Inputs'
|
||||
|
||||
export const ProfilePage = () => {
|
||||
return <h1>WIP</h1>
|
||||
// Try to decode nprofile parameter
|
||||
const { nprofile } = useParams()
|
||||
let profilePubkey: string | undefined
|
||||
try {
|
||||
const value = nprofile
|
||||
? nip19.decode(nprofile as `nprofile1${string}`)
|
||||
: undefined
|
||||
profilePubkey = value?.data.pubkey
|
||||
} catch (error) {
|
||||
// Failed to decode the nprofile
|
||||
// Silently ignore and redirect to home or logged in user
|
||||
}
|
||||
|
||||
const scrollTargetRef = useRef<HTMLDivElement>(null)
|
||||
const { ndk, publish, findMetadata, fetchEventFromUserRelays, fetchMods } =
|
||||
useNDKContext()
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
const isOwnProfile =
|
||||
userState.auth && userState.user?.pubkey === profilePubkey
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||
|
||||
useDidMount(() => {
|
||||
if (profilePubkey) {
|
||||
findMetadata(profilePubkey).then((res) => {
|
||||
setProfile(res)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const displayName =
|
||||
profile?.displayName || profile?.name || '[name not set up]'
|
||||
const [showReportPopUp, setShowReportPopUp] = useState(false)
|
||||
|
||||
const [isBlocked, setIsBlocked] = useState(false)
|
||||
useEffect(() => {
|
||||
if (userState.auth && userState.user?.pubkey) {
|
||||
const userHexKey = userState.user.pubkey as string
|
||||
|
||||
const muteListFilter: NDKFilter = {
|
||||
kinds: [NDKKind.MuteList],
|
||||
authors: [userHexKey]
|
||||
}
|
||||
|
||||
fetchEventFromUserRelays(
|
||||
muteListFilter,
|
||||
userHexKey,
|
||||
UserRelaysType.Write
|
||||
).then((event) => {
|
||||
if (event) {
|
||||
// get a list of tags
|
||||
const tags = event.tags
|
||||
const blocked =
|
||||
tags.findIndex(
|
||||
(item) => item[0] === 'p' && item[1] === profilePubkey
|
||||
) !== -1
|
||||
|
||||
setIsBlocked(blocked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [userState, profilePubkey, fetchEventFromUserRelays])
|
||||
|
||||
const handleBlock = async () => {
|
||||
if (!profilePubkey) {
|
||||
toast.error(`Something went wrong. Unable to find reported user's pubkey`)
|
||||
return
|
||||
}
|
||||
|
||||
let userHexKey: string
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc('Getting user pubkey')
|
||||
|
||||
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 updating mute list')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
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] === profilePubkey
|
||||
) !== -1
|
||||
|
||||
if (alreadyExists) {
|
||||
setIsLoading(false)
|
||||
setIsBlocked(true)
|
||||
return toast.warn(`User is already in the mute list`)
|
||||
}
|
||||
|
||||
tags.push(['p', profilePubkey])
|
||||
|
||||
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', profilePubkey]]
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Updating mute list event')
|
||||
|
||||
const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
|
||||
if (isUpdated) {
|
||||
setIsBlocked(true)
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleUnblock = async () => {
|
||||
if (!profilePubkey) {
|
||||
toast.error(`Something went wrong. Unable to find reported user's pubkey`)
|
||||
return
|
||||
}
|
||||
|
||||
const userHexKey = userState.user?.pubkey as string
|
||||
|
||||
const filter: NDKFilter = {
|
||||
kinds: [NDKKind.MuteList],
|
||||
authors: [userHexKey]
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
||||
|
||||
// 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
|
||||
)
|
||||
|
||||
if (!muteListEvent) {
|
||||
toast.error(`Couldn't get user's mute list event from relays`)
|
||||
return
|
||||
}
|
||||
|
||||
const tags = muteListEvent.tags
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
pubkey: muteListEvent.pubkey,
|
||||
kind: NDKKind.MuteList,
|
||||
content: muteListEvent.content,
|
||||
created_at: now(),
|
||||
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== profilePubkey)
|
||||
}
|
||||
|
||||
setLoadingSpinnerDesc('Updating mute list event')
|
||||
const isUpdated = await signAndPublish(unsignedEvent, ndk, publish)
|
||||
if (isUpdated) {
|
||||
setIsBlocked(false)
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// Tabs
|
||||
const [tab, setTab] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// Mods
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW,
|
||||
source: window.location.host,
|
||||
moderated: ModeratedFilter.Moderated,
|
||||
author: profilePubkey
|
||||
})
|
||||
const muteLists = useMuteLists()
|
||||
const nsfwList = useNSFWList()
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setIsLoading(true)
|
||||
|
||||
const until =
|
||||
mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined
|
||||
|
||||
fetchMods({
|
||||
source: filterOptions.source,
|
||||
until,
|
||||
author: profilePubkey
|
||||
})
|
||||
.then((res) => {
|
||||
setMods(res)
|
||||
setPage((prev) => prev + 1)
|
||||
scrollIntoView(scrollTargetRef.current)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [mods, fetchMods, filterOptions.source, profilePubkey])
|
||||
|
||||
const handlePrev = useCallback(() => {
|
||||
setIsLoading(true)
|
||||
|
||||
const since = mods.length > 0 ? mods[0].published_at + 1 : undefined
|
||||
|
||||
fetchMods({
|
||||
source: filterOptions.source,
|
||||
since,
|
||||
author: profilePubkey
|
||||
})
|
||||
.then((res) => {
|
||||
setMods(res)
|
||||
setPage((prev) => prev - 1)
|
||||
scrollIntoView(scrollTargetRef.current)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [mods, fetchMods, filterOptions.source, profilePubkey])
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
switch (tab) {
|
||||
case 0:
|
||||
fetchMods({ source: filterOptions.source, author: profilePubkey })
|
||||
.then((res) => {
|
||||
setMods(res)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false)
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
setIsLoading(false)
|
||||
break
|
||||
}
|
||||
}, [filterOptions.source, tab, fetchMods, profilePubkey])
|
||||
const filteredModList = useFilteredMods(
|
||||
mods,
|
||||
userState,
|
||||
filterOptions,
|
||||
nsfwList,
|
||||
muteLists
|
||||
)
|
||||
|
||||
// Redirect route
|
||||
let profileRoute = appRoutes.home
|
||||
if (!nprofile && userState.auth && userState.user) {
|
||||
// Redirect to user's profile is no profile is linked
|
||||
const userHexKey = npubToHex(userState.user.npub as string)
|
||||
|
||||
if (userHexKey) {
|
||||
profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: userHexKey
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!profilePubkey) return <Navigate to={profileRoute} replace={true} />
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div
|
||||
className='IBMSecMainGroup IBMSecMainGroupAlt'
|
||||
ref={scrollTargetRef}
|
||||
>
|
||||
<div className='IBMSMSplitMain'>
|
||||
<div className='IBMSMSplitMainBigSide'>
|
||||
<div className='IBMSMSplitMainBigSideSec'>
|
||||
<div className='IBMSMSMBSS_Profile'>
|
||||
<div className='IBMSMSMBSSModFor'>
|
||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
<p className='IBMSMSMBSSModForPara'>{displayName}'s Page</p>
|
||||
<div
|
||||
className='dropdown dropdownMain'
|
||||
style={{ flexGrow: 'unset' }}
|
||||
>
|
||||
<button
|
||||
className='btn btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
style={{
|
||||
borderRadius: '5px',
|
||||
background: 'unset',
|
||||
padding: '5px'
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-192 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M64 360C94.93 360 120 385.1 120 416C120 446.9 94.93 472 64 472C33.07 472 8 446.9 8 416C8 385.1 33.07 360 64 360zM64 200C94.93 200 120 225.1 120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200zM64 152C33.07 152 8 126.9 8 96C8 65.07 33.07 40 64 40C94.93 40 120 65.07 120 96C120 126.9 94.93 152 64 152z'></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div className='dropdown-menu dropdown-menu-end dropdownMainMenu'>
|
||||
{isOwnProfile && (
|
||||
<Link
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
to={appRoutes.settingsProfile}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path>
|
||||
</svg>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() => {
|
||||
copyTextToClipboard(window.location.href).then(
|
||||
(isCopied) => {
|
||||
if (isCopied)
|
||||
toast.success('Url copied to clipboard!')
|
||||
}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
|
||||
</svg>
|
||||
Copy URL
|
||||
</a>
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
href='#'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M503.7 226.2l-176 151.1c-15.38 13.3-39.69 2.545-39.69-18.16V272.1C132.9 274.3 66.06 312.8 111.4 457.8c5.031 16.09-14.41 28.56-28.06 18.62C39.59 444.6 0 383.8 0 322.3c0-152.2 127.4-184.4 288-186.3V56.02c0-20.67 24.28-31.46 39.69-18.16l176 151.1C514.8 199.4 514.8 216.6 503.7 226.2z'></path>
|
||||
</svg>
|
||||
Share
|
||||
</a>
|
||||
{!isOwnProfile && (
|
||||
<>
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportUser'
|
||||
onClick={() => setShowReportPopUp(true)}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||
</svg>
|
||||
Report
|
||||
</a>
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={isBlocked ? handleUnblock : handleBlock}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-32 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M323.5 51.25C302.8 70.5 284 90.75 267.4 111.1C240.1 73.62 206.2 35.5 168 0C69.75 91.12 0 210 0 281.6C0 408.9 100.2 512 224 512s224-103.1 224-230.4C448 228.4 396 118.5 323.5 51.25zM304.1 391.9C282.4 407 255.8 416 226.9 416c-72.13 0-130.9-47.73-130.9-125.2c0-38.63 24.24-72.64 72.74-130.8c7 8 98.88 125.4 98.88 125.4l58.63-66.88c4.125 6.75 7.867 13.52 11.24 19.9C364.9 290.6 353.4 357.4 304.1 391.9z'></path>
|
||||
</svg>
|
||||
{isBlocked ? 'Unblock' : 'Block User'}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
tabs={['Mods', 'Blogs', 'Posts']}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
/>
|
||||
|
||||
{/* Tabs Content */}
|
||||
{tab === 0 && (
|
||||
<>
|
||||
<ModFilter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
|
||||
<div className='IBMSMList IBMSMListAlt'>
|
||||
{filteredModList.map((mod) => (
|
||||
<ModCard key={mod.id} {...mod} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={mods.length < MOD_FILTER_LIMIT}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === 1 && <>WIP</>}
|
||||
{tab === 2 && <>WIP</>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProfileSection pubkey={profilePubkey} />
|
||||
</div>
|
||||
</div>
|
||||
{showReportPopUp && (
|
||||
<ReportUserPopup
|
||||
reportedPubkey={profilePubkey}
|
||||
handleClose={() => setShowReportPopUp(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
<div className='popUpMainCard popUpMainCardQR'>
|
||||
<div className='popUpMainCardTop'>
|
||||
<div className='popUpMainCardTopInfo'>
|
||||
<h3>Report Post</h3>
|
||||
</div>
|
||||
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-96 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pUMCB_Zaps'>
|
||||
<div className='pUMCB_ZapsInside'>
|
||||
<div className='inputLabelWrapperMain'>
|
||||
<label
|
||||
className='form-label labelMain'
|
||||
style={{ fontWeight: 'bold' }}
|
||||
>
|
||||
Why are you reporting the user?
|
||||
</label>
|
||||
{USER_REPORT_REASONS.map((r) => (
|
||||
<CheckboxField
|
||||
key={r.key}
|
||||
label={r.label}
|
||||
name={r.key}
|
||||
isChecked={selectedOptions[r.key]}
|
||||
handleChange={() => handleCheckboxChange(r.key)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
className='btn btnMain pUMCB_Report'
|
||||
type='button'
|
||||
style={{ width: '100%' }}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ export const WritePage = () => {
|
||||
name='nsfw'
|
||||
isChecked={false}
|
||||
handleChange={() => {}}
|
||||
type='stylized'
|
||||
/>
|
||||
<div className='IBMSMSMBS_WriteAction'>
|
||||
<button className='btn btnMain' type='button'>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { createBrowserRouter } from 'react-router-dom'
|
||||
import { Layout } from 'layout'
|
||||
import { SearchPage } from 'pages/search'
|
||||
import { AboutPage } from '../pages/about'
|
||||
import { BlogsPage } from '../pages/blogs'
|
||||
@ -10,10 +12,14 @@ import { SettingsPage } from '../pages/settings'
|
||||
import { SubmitModPage } from '../pages/submitMod'
|
||||
import { WritePage } from '../pages/write'
|
||||
import { GamePage } from 'pages/game'
|
||||
import { NotFoundPage } from 'pages/404'
|
||||
import { FeedLayout } from 'layout/feed'
|
||||
import { FeedPage } from 'pages/feed'
|
||||
import { NotificationsPage } from 'pages/notifications'
|
||||
|
||||
export const appRoutes = {
|
||||
index: '/',
|
||||
home: '/home',
|
||||
home: '/',
|
||||
games: '/games',
|
||||
game: '/game/:name',
|
||||
mods: '/mods',
|
||||
@ -28,7 +34,9 @@ export const appRoutes = {
|
||||
settingsRelays: '/settings-relays',
|
||||
settingsPreferences: '/settings-preferences',
|
||||
settingsAdmin: '/settings-admin',
|
||||
profile: '/profile/:nprofile'
|
||||
profile: '/profile/:nprofile?',
|
||||
feed: '/feed',
|
||||
notifications: '/notifications'
|
||||
}
|
||||
|
||||
export const getGamePageRoute = (name: string) =>
|
||||
@ -43,15 +51,14 @@ export const getModsEditPageRoute = (eventId: string) =>
|
||||
export const getProfilePageRoute = (nprofile: string) =>
|
||||
appRoutes.profile.replace(':nprofile', nprofile)
|
||||
|
||||
export const routes = [
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
element: <Layout />,
|
||||
children: [
|
||||
{
|
||||
path: appRoutes.index,
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.home,
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.games,
|
||||
element: <GamesPage />
|
||||
@ -111,5 +118,24 @@ export const routes = [
|
||||
{
|
||||
path: appRoutes.profile,
|
||||
element: <ProfilePage />
|
||||
},
|
||||
{
|
||||
element: <FeedLayout />,
|
||||
children: [
|
||||
{
|
||||
path: appRoutes.feed,
|
||||
element: <FeedPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.notifications,
|
||||
element: <NotificationsPage />
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <NotFoundPage />
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
@ -22,6 +22,5 @@
|
||||
flex-wrap: wrap;
|
||||
grid-gap: 10px;
|
||||
padding: 10px 0 0 0;
|
||||
border-top: solid 1px rgba(255,255,255,0.1);
|
||||
border-top: solid 1px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
|
@ -22,4 +22,5 @@ export interface FilterOptions {
|
||||
nsfw: NSFWFilter
|
||||
source: string
|
||||
moderated: ModeratedFilter
|
||||
author?: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user