Merge pull request 'try again button, user blog tab load, no game text' (#181) from staging into master
Some checks failed
Release to Production / build_and_release (push) Failing after 57s

Reviewed-on: #181
This commit is contained in:
freakoverse 2025-01-03 11:29:54 +00:00
commit 0c7c2323f2
23 changed files with 336 additions and 79 deletions

View File

@ -1,11 +1,12 @@
import { RouterProvider } from 'react-router-dom'
import { useEffect } from 'react'
import { routerWithNdkContext } from 'routes'
import { useEffect, useMemo } from 'react'
import { routerWithNdkContext as routerWithState } from 'routes'
import { useNDKContext } from 'hooks'
import './styles/styles.css'
function App() {
const ndkContext = useNDKContext()
const router = useMemo(() => routerWithState(ndkContext), [ndkContext])
useEffect(() => {
// Find the element with id 'root'
@ -24,7 +25,7 @@ function App() {
}
}, [])
return <RouterProvider router={routerWithNdkContext(ndkContext)} />
return <RouterProvider router={router} />
}
export default App

View File

@ -8,5 +8,6 @@
"total conversions",
"translation",
"multiplayer",
"clothing"
"clothing",
"Mod Manager"
]

View File

@ -22391,8 +22391,7 @@ Climbing Over It with a Spear,,
Land of the Survivors Demo,,
Nie No Hakoniwa,,
Arcana Alchemia,,
MiSide,,
MiSide Demo,,
MiSide,,https://i.imgur.com/CAOt6Lc.jpeg
Uncharted Ocean 2,,
Roll The Bones,,
Farm Manager World Playtest,,

Can't render this file because it is too large.

View File

@ -222,7 +222,7 @@ export const ModForm = () => {
<InputField
label='Featured Image URL'
description='We recommend to upload images to https://nostr.build/'
description='We recommend to upload images to https://imgur.com/upload'
type='text'
inputMode='url'
placeholder='Image URL'
@ -293,7 +293,7 @@ export const ModForm = () => {
</button>
</div>
<p className='labelDescriptionMain'>
We recommend to upload images to https://nostr.build/
We recommend to upload images to https://imgur.com/upload
</p>
{formState.screenshotsUrls.map((url, index) => (
<Fragment key={`screenShot-${index}`}>
@ -607,7 +607,7 @@ const ScreenshotUrlFields = React.memo(
type='text'
className='inputMain'
inputMode='url'
placeholder='We recommend to upload images to https://nostr.build/'
placeholder='We recommend to upload images to https://imgur.com/upload'
value={url}
onChange={handleChange}
/>

View File

@ -252,7 +252,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
})
.catch((err) => {
log(
true,
false, // Too many failed requests, turned off for clarity
LogType.Error,
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
err

View File

@ -39,7 +39,7 @@ export const useComments = (
})
.catch((err) => {
log(
true,
false, // Too many failed requests, turned off for clarity
LogType.Error,
`An error occurred in fetching user's (${author}) ${UserRelaysType.Read}`,
err

View File

@ -1,4 +1,6 @@
import { Link } from 'react-router-dom'
import styles from '../styles/footer.module.scss'
import { appRoutes, getProfilePageRoute } from 'routes'
export const Footer = () => {
return (
@ -7,6 +9,7 @@ export const Footer = () => {
<p className={styles.secMainFooterPara}>
Built with&nbsp;
<a
rel='noopener'
className={styles.secMainFooterParaLink}
href='https://github.com/nostr-protocol/nostr'
target='_blank'
@ -14,21 +17,26 @@ export const Footer = () => {
Nostr
</a>{' '}
by&nbsp;
<a
<Link
className={styles.secMainFooterParaLink}
href='https://degmods.com/profile/nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp'
to={getProfilePageRoute(
'nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp'
)}
target='_blank'
>
Freakoverse
</a>
</Link>
, with the support of{' '}
<a className={styles.secMainFooterParaLink} href='backers.html'>
<Link
className={styles.secMainFooterParaLink}
to={appRoutes.supporters}
>
Supporters
</a>
</Link>
. Check our&nbsp;
<a className={styles.secMainFooterParaLink} href='backup.html'>
<Link className={styles.secMainFooterParaLink} to={appRoutes.backup}>
Backup Plan
</a>
</Link>
.
</p>
</div>

View File

@ -451,6 +451,8 @@ const RegisterButtonWithDialog = () => {
A: DEG Mods can't ban you or delete your content (we can
only hide you), and the consequence of that is this kind of
registration/login system.
<br /><br />
Warning:&nbsp;Make sure you backup your private key somewhere safe. If you lose it or it gets leaked, we actually can't help you.
</p>
<div className='dividerPopup'>
<div className='dividerPopupLine'></div>

View File

@ -44,7 +44,7 @@ export const Layout = () => {
})
}
}
}, [ndk, dispatch])
}, [dispatch, ndk])
// calculate user's wot
useEffect(() => {
@ -60,7 +60,7 @@ export const Layout = () => {
toast.error('An error occurred in calculating user web-of-trust!')
})
}
}, [ndk, userState.user, dispatch])
}, [dispatch, ndk, userState.user?.pubkey])
// get site's wot level
useEffect(() => {
@ -106,7 +106,7 @@ export const Layout = () => {
})
}
}
}, [userState.user, dispatch, fetchEventFromUserRelays])
}, [dispatch, fetchEventFromUserRelays, userState.user?.pubkey])
return (
<>

View File

@ -1,4 +1,4 @@
import { Link, useRouteError } from 'react-router-dom'
import { Link, useLocation, useRouteError } from 'react-router-dom'
import { appRoutes } from 'routes'
interface NotFoundPageProps {
@ -12,6 +12,8 @@ export const NotFoundPage = ({
}: Partial<NotFoundPageProps>) => {
const error = useRouteError() as Partial<NotFoundPageProps>
const location = useLocation()
return (
<div className='InnerBodyMain'>
<div className='ContainerMain'>
@ -23,7 +25,19 @@ export const NotFoundPage = ({
<div>
<p>{error?.message || message}</p>
</div>
<div className='IBMSMAction'>
<div
className='IBMSMAction'
style={{
gap: '10px'
}}
>
<Link
to={location.pathname}
className='btn btnMain IBMSMActionBtn'
type='button'
>
Try again
</Link>
<Link
to={appRoutes.home}
className='btn btnMain IBMSMActionBtn'

126
src/pages/backup.tsx Normal file
View File

@ -0,0 +1,126 @@
import { capitalizeEachWord } from 'utils'
import '../styles/backup.css'
import backupPlanImg from '../assets/img/DEG Mods Backup Plan.png'
// import placeholder from '../assets/img/DEGMods Placeholder Img.png'
interface BackupItemProps {
name: string
image: string
link: string
type: 'repo' | 'alt' | 'exe'
}
const BACKUP_LIST: BackupItemProps[] = [
// {
// name: 'Github',
// type: 'repo',
// image:
// 'https://www.c-sharpcorner.com/article/create-github-repository-and-add-newexisting-project-using-github-desktop/Images/github.png',
// link: '#'
// },
// {
// name: 'Github, but nostr',
// type: 'repo',
// image: 'https://vitorpamplona.com/images/nostr.gif',
// link: '#'
// },
// {
// name: 'name',
// type: 'alt',
// image: placeholder,
// link: '#'
// },
// {
// name: '',
// type: 'exe',
// image: placeholder,
// link: '#'
// }
]
const BackupItem = ({ name, image, link, type }: BackupItemProps) => {
return (
<a
className='backupListLink'
href={link}
style={{
background: `linear-gradient(15deg, rgba(0,0,0,0.75), rgba(0,0,0,0.25)),
url("${image}") center / cover no-repeat,
linear-gradient(45deg, rgba(0,0,0,0.1), rgba(255,255,255,0.01) 50%, rgba(0,0,0,0.1))`
}}
target='_blank'
>
<div className='backupListLinkInside'>
<h3>
{type === 'exe' ? type.toUpperCase() : capitalizeEachWord(type)}:{' '}
{name}
</h3>
</div>
</a>
)
}
export const BackupPage = () => {
return (
<div className='InnerBodyMain'>
<div className='ContainerMain'>
<div className='IBMSecMainGroup'>
<div className='IBMSecMain'>
<div className='AboutSec'>
<div className='LearnText'>
<div className='LearnTextInside'>
<h1
className='LearnTextHeading'
style={{ textAlign: 'center' }}
>
Backup Plan: Repos, Alts, EXE
</h1>
<img alt='' src={backupPlanImg} />
<p className='LearnTextPara'>
It's pretty clear that authoritarianism and censorship is on
the rise, on all fronts, and from what can be seen, any idea
that push for the opposite gets attacked. That's why DEG
Mods is running on Nostr, and that's why we're also writing
this backup plan.
<br />
</p>
<h3 className='LearnTextHeading'>Repositories</h3>
<p className='LearnTextPara'>
Wherever we can, we'll put DEG Mods' code on multiple
repositories such as Github, and (github but on nostr).
Below you can find the links where we've uploaded the site's
code to.
<br />
</p>
<h3 className='LearnTextHeading'>Alternatives</h3>
<p className='LearnTextPara'>
With the repositories for DEG Mods is up on multiple places,
we encourage people to take the code and duplicate it
elsewhere. Fork it, change the design, remove or add systems
and features, and make your own version. Below you can find
links of alts that we've found.
<br />
</p>
<h3 className='LearnTextHeading'>EXE</h3>
<p className='LearnTextPara'>
One last push we'd like to do is to create a .exe that'll
open up DEG Mods on your PC, as if you've opened the website
normally, with almost all of the functionalities you'd
expect (if not all). We want to do this so that in case
there are no alternatives, or that they're getting shut
down, then you can just rely on this instead. The link to it
will be added here the moment it becomes available.
<br />
</p>
<div className='backupList'>
{BACKUP_LIST.map((b) => (
<BackupItem {...b} />
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -13,6 +13,7 @@ import {
} from 'types'
import {
DEFAULT_FILTER_OPTIONS,
getFallbackPubkey,
getLocalStorageItem,
log,
LogType
@ -41,7 +42,8 @@ export const blogRouteLoader =
}
const userState = store.getState().user
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined
const loggedInUserPubkey =
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
// Check if editing and the user is the original author
// Redirect if NOT

View File

@ -16,10 +16,12 @@ import {
DEFAULT_FILTER_OPTIONS,
extractBlogCardDetails,
extractModData,
getFallbackPubkey,
getLocalStorageItem,
getReportingSet,
log,
LogType
LogType,
timeout
} from 'utils'
export const modRouteLoader =
@ -46,7 +48,8 @@ export const modRouteLoader =
}
const userState = store.getState().user
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined
const loggedInUserPubkey =
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
try {
// Set up the filters
@ -82,8 +85,8 @@ export const modRouteLoader =
// Parallel fetch mod event, latest events, mute, nsfw, repost lists
const settled = await Promise.allSettled([
ndkContext.fetchEvent(modFilter),
ndkContext.fetchEvents(latestFilter),
Promise.race([ndkContext.fetchEvent(modFilter), timeout(2000)]),
Promise.race([ndkContext.fetchEvents(latestFilter), timeout(2000)]),
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)

View File

@ -1,7 +1,13 @@
import { NDKContextType } from 'contexts/NDKContext'
import { store } from 'store'
import { MuteLists } from 'types'
import { getReportingSet, CurationSetIdentifiers, log, LogType } from 'utils'
import {
getReportingSet,
CurationSetIdentifiers,
log,
LogType,
getFallbackPubkey
} from 'utils'
export interface ModsPageLoaderResult {
muteLists: {
@ -31,15 +37,11 @@ export const modsRouteLoader = (ndkContext: NDKContextType) => async () => {
// Get the current state
const userState = store.getState().user
// Check if current user is logged in
let userPubkey: string | undefined
if (userState.auth && userState.user?.pubkey) {
userPubkey = userState.user.pubkey as string
}
const loggedInUserPubkey =
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
const settled = await Promise.allSettled([
ndkContext.getMuteLists(userPubkey),
ndkContext.getMuteLists(loggedInUserPubkey),
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)
])

View File

@ -46,7 +46,6 @@ export const ProfilePage = () => {
profilePubkey,
profile,
isBlocked: _isBlocked,
isOwnProfile,
repostList,
muteLists,
nsfwList
@ -60,6 +59,10 @@ export const ProfilePage = () => {
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const [showReportPopUp, setShowReportPopUp] = useState(false)
const isOwnProfile =
userState.auth &&
userState.user?.pubkey &&
userState.user.pubkey === profilePubkey
const [isBlocked, setIsBlocked] = useState(_isBlocked)
const handleBlock = async () => {
@ -661,7 +664,7 @@ const ReportUserPopup = ({
}
const ProfileTabBlogs = () => {
const { profile, muteLists, nsfwList } =
const { profilePubkey, muteLists, nsfwList } =
useLoaderData() as ProfilePageLoaderResult
const navigation = useNavigation()
const { fetchEvents } = useNDKContext()
@ -669,7 +672,7 @@ const ProfileTabBlogs = () => {
const [isLoading, setIsLoading] = useState(true)
const blogfilter: NDKFilter = useMemo(() => {
const filter: NDKFilter = {
authors: [profile?.pubkey as string],
authors: [profilePubkey],
kinds: [kinds.LongFormArticle]
}
@ -683,13 +686,13 @@ const ProfileTabBlogs = () => {
}
return filter
}, [filterOptions.nsfw, filterOptions.source, profile?.pubkey])
}, [filterOptions.nsfw, filterOptions.source, profilePubkey])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>([])
useEffect(() => {
if (profile) {
if (profilePubkey) {
// Initial blog fetch, go beyond limit to check for next
const filter: NDKFilter = {
...blogfilter,
@ -704,7 +707,7 @@ const ProfileTabBlogs = () => {
setIsLoading(false)
})
}
}, [blogfilter, fetchEvents, profile])
}, [blogfilter, fetchEvents, profilePubkey])
const handleNext = useCallback(() => {
if (isLoading) return
@ -758,7 +761,7 @@ const ProfileTabBlogs = () => {
let _blogs = blogs || []
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isOwner =
userState.user?.pubkey && userState.user.pubkey === profile?.pubkey
userState.user?.pubkey && userState.user.pubkey === profilePubkey
const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
@ -815,7 +818,7 @@ const ProfileTabBlogs = () => {
muteLists.user.authors,
muteLists.user.replaceableEvents,
nsfwList,
profile?.pubkey,
profilePubkey,
userState.user?.npub,
userState.user?.pubkey
])
@ -826,10 +829,7 @@ const ProfileTabBlogs = () => {
<LoadingSpinner desc={'Loading...'} />
)}
<BlogsFilter
filterKey={'filter-blog'}
author={profile?.pubkey as string}
/>
<BlogsFilter filterKey={'filter-blog'} author={profilePubkey} />
<div className='IBMSMList IBMSMListAlt'>
{moderatedAndSortedBlogs.map((b) => (

View File

@ -6,6 +6,7 @@ import { store } from 'store'
import { MuteLists, UserProfile } from 'types'
import {
CurationSetIdentifiers,
getFallbackPubkey,
getReportingSet,
log,
LogType,
@ -16,7 +17,6 @@ export interface ProfilePageLoaderResult {
profilePubkey: string
profile: UserProfile
isBlocked: boolean
isOwnProfile: boolean
muteLists: {
admin: MuteLists
user: MuteLists
@ -58,21 +58,17 @@ export const profileRouteLoader =
// Get the current state
const userState = store.getState().user
// Check if current user is logged in
let userPubkey: string | undefined
if (userState.auth && userState.user?.pubkey) {
userPubkey = userState.user.pubkey as string
}
const loggedInUserPubkey =
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
// Redirect if profile naddr is missing
// - home if user is not logged
let profileRoute = appRoutes.home
if (!profilePubkey && userPubkey) {
if (!profilePubkey && loggedInUserPubkey) {
// - own profile
profileRoute = getProfilePageRoute(
nip19.nprofileEncode({
pubkey: userPubkey
pubkey: loggedInUserPubkey
})
)
}
@ -83,7 +79,6 @@ export const profileRouteLoader =
profilePubkey: profilePubkey,
profile: {},
isBlocked: false,
isOwnProfile: false,
muteLists: {
admin: {
authors: [],
@ -98,14 +93,9 @@ export const profileRouteLoader =
repostList: []
}
// Check if user the user is logged in
if (userState.auth && userState.user?.pubkey) {
result.isOwnProfile = userState.user.pubkey === profilePubkey
}
const settled = await Promise.allSettled([
ndkContext.findMetadata(profilePubkey),
ndkContext.getMuteLists(userPubkey),
ndkContext.getMuteLists(loggedInUserPubkey),
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
getReportingSet(CurationSetIdentifiers.Repost, ndkContext)
])

View File

@ -39,6 +39,7 @@ import {
scrollIntoView
} from 'utils'
import { useCuratedSet } from 'hooks/useCuratedSet'
import dedup from 'utils/nostr'
enum SearchKindEnum {
Mods = 'Mods',
@ -508,6 +509,11 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
return (
<>
{searchTerm !== '' && filteredGames.length === 0 && (
<div className='IBMSecMain IBMSMListWrapper'>
Game not found. Send us a message where you can reach us to add it
</div>
)}
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList IBMSMListFeaturedAlt'>
{filteredGames
@ -521,20 +527,14 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
))}
</div>
</div>
<Pagination
page={page}
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
handlePrev={handlePrev}
handleNext={handleNext}
/>
{searchTerm !== '' && filteredGames.length > MAX_GAMES_PER_PAGE && (
<Pagination
page={page}
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
handlePrev={handlePrev}
handleNext={handleNext}
/>
)}
</>
)
}
function dedup(event1: NDKEvent, event2: NDKEvent) {
// return the newest of the two
if (event1.created_at! > event2.created_at!) {
return event1
}
return event2
}

3
src/pages/supporters.tsx Normal file
View File

@ -0,0 +1,3 @@
export const SupportersPage = () => {
return <h2>WIP</h2>
}

View File

@ -28,6 +28,8 @@ import { BlogPage } from '../pages/blog'
import { blogRouteLoader } from '../pages/blog/loader'
import { blogRouteAction } from '../pages/blog/action'
import { reportRouteAction } from '../actions/report'
import { BackupPage } from 'pages/backup'
import { SupportersPage } from 'pages/supporters'
export const appRoutes = {
home: '/',
@ -51,7 +53,9 @@ export const appRoutes = {
settingsAdmin: '/settings-admin',
profile: '/profile/:nprofile?',
feed: '/feed',
notifications: '/notifications'
notifications: '/notifications',
backup: '/backup',
supporters: '/supporters'
}
export const getGamePageRoute = (name: string) =>
@ -185,6 +189,14 @@ export const routerWithNdkContext = (context: NDKContextType) =>
}
]
},
{
path: appRoutes.backup,
element: <BackupPage />
},
{
path: appRoutes.supporters,
element: <SupportersPage />
},
{
path: '*',
element: <NotFoundPage />

36
src/styles/backup.css Normal file
View File

@ -0,0 +1,36 @@
.backupList {
width: 100%;
display: flex;
flex-direction: column;
grid-gap: 15px;
}
.backupListLink {
transition: ease 0.4s;
overflow: hidden;
padding: 15px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
border-radius: 10px;
/*border: solid 1px rgba(255,255,255,0.1);*/
position: relative;
color: rgba(255,255,255,0.75);
min-height: 150px;
display: flex;
flex-direction: column;
}
.backupListLink:hover {
transition: ease 0.4s;
text-decoration: unset;
color: rgb(255,255,255);
transform: scale(1.02);
}
.backupListLinkInside {
display: flex;
flex-direction: column;
grid-gap: 0px;
flex-grow: 1;
justify-content: end;
}

View File

@ -705,3 +705,28 @@ a:hover {
transform: rotate(360deg);
}
}
.uploadBoxMain {
background: hsl(0deg 0% 0% / 10%);
border-radius: 10px;
height: 10px;
padding: 10px;
border: solid 1px hsl(0deg 0% 100% / 5%);
}
.uploadBoxMain:hover > .uploadBoxMainInside {
padding: 5px;
}
.uploadBoxMainInside {
padding: 10px;
border: dashed 2px hsl(0deg 0% 100% / 5%);
border-radius: 8px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: hsl(0deg 0% 100% / 20%);
grid-gap: 10px;
}

View File

@ -274,3 +274,16 @@ export function orderEventsChronologically(
return events
}
/**
* Receives two events and returns the "correct" event to use.
* #nip-33
*/
export default function dedup(event1: NDKEvent, event2: NDKEvent) {
// return the newest of the two
if (event1.created_at! > event2.created_at!) {
return event1
}
return event2
}

View File

@ -160,3 +160,23 @@ export const parseFormData = <T>(formData: FormData) => {
export const capitalizeEachWord = (str: string): string => {
return str.replace(/\b\w/g, (char) => char.toUpperCase())
}
/**
* nostr-login - helper function
* should only be used as the fallback
* user state is not updated before `onAuth` triggers but loaders are faster
*/
export const getFallbackPubkey = () => {
try {
// read nostr-login conf from localStorage
const stored = window.localStorage.getItem('__nostrlogin_nip46')
if (!stored) return
const info = JSON.parse(stored)
if (info && !info.pubkey) return
return info.pubkey as string
} catch {
// Silently ignore
}
}