try again button, user blog tab load, no game text #181

Merged
freakoverse merged 14 commits from staging into master 2025-01-03 11:29:54 +00:00
17 changed files with 283 additions and 68 deletions
Showing only changes of commit 50f9800935 - Show all commits

View File

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

View File

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

View File

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

View File

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

View File

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

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' } from 'types'
import { import {
DEFAULT_FILTER_OPTIONS, DEFAULT_FILTER_OPTIONS,
getFallbackPubkey,
getLocalStorageItem, getLocalStorageItem,
log, log,
LogType LogType
@ -41,7 +42,8 @@ export const blogRouteLoader =
} }
const userState = store.getState().user 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 // Check if editing and the user is the original author
// Redirect if NOT // Redirect if NOT

View File

@ -16,6 +16,7 @@ import {
DEFAULT_FILTER_OPTIONS, DEFAULT_FILTER_OPTIONS,
extractBlogCardDetails, extractBlogCardDetails,
extractModData, extractModData,
getFallbackPubkey,
getLocalStorageItem, getLocalStorageItem,
getReportingSet, getReportingSet,
log, log,
@ -46,7 +47,8 @@ export const modRouteLoader =
} }
const userState = store.getState().user const userState = store.getState().user
const loggedInUserPubkey = userState?.user?.pubkey as string | undefined const loggedInUserPubkey =
(userState?.user?.pubkey as string | undefined) || getFallbackPubkey()
try { try {
// Set up the filters // Set up the filters

View File

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

View File

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

View File

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

View File

@ -39,6 +39,7 @@ import {
scrollIntoView scrollIntoView
} from 'utils' } from 'utils'
import { useCuratedSet } from 'hooks/useCuratedSet' import { useCuratedSet } from 'hooks/useCuratedSet'
import dedup from 'utils/nostr'
enum SearchKindEnum { enum SearchKindEnum {
Mods = 'Mods', Mods = 'Mods',
@ -508,6 +509,11 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
return ( 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='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList IBMSMListFeaturedAlt'> <div className='IBMSMList IBMSMListFeaturedAlt'>
{filteredGames {filteredGames
@ -521,20 +527,14 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
))} ))}
</div> </div>
</div> </div>
<Pagination {searchTerm !== '' && filteredGames.length > MAX_GAMES_PER_PAGE && (
page={page} <Pagination
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE} page={page}
handlePrev={handlePrev} disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
handleNext={handleNext} 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 { blogRouteLoader } from '../pages/blog/loader'
import { blogRouteAction } from '../pages/blog/action' import { blogRouteAction } from '../pages/blog/action'
import { reportRouteAction } from '../actions/report' import { reportRouteAction } from '../actions/report'
import { BackupPage } from 'pages/backup'
import { SupportersPage } from 'pages/supporters'
export const appRoutes = { export const appRoutes = {
home: '/', home: '/',
@ -51,7 +53,9 @@ export const appRoutes = {
settingsAdmin: '/settings-admin', settingsAdmin: '/settings-admin',
profile: '/profile/:nprofile?', profile: '/profile/:nprofile?',
feed: '/feed', feed: '/feed',
notifications: '/notifications' notifications: '/notifications',
backup: '/backup',
supporters: '/supporters'
} }
export const getGamePageRoute = (name: string) => 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: '*', path: '*',
element: <NotFoundPage /> 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

@ -274,3 +274,16 @@ export function orderEventsChronologically(
return events 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 => { export const capitalizeEachWord = (str: string): string => {
return str.replace(/\b\w/g, (char) => char.toUpperCase()) 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
}
}