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 { 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

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

View File

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

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

View File

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

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

@ -451,6 +451,8 @@ const RegisterButtonWithDialog = () => {
A: DEG Mods can't ban you or delete your content (we can 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 only hide you), and the consequence of that is this kind of
registration/login system. 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> </p>
<div className='dividerPopup'> <div className='dividerPopup'>
<div className='dividerPopupLine'></div> <div className='dividerPopupLine'></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 (
<> <>

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' import { appRoutes } from 'routes'
interface NotFoundPageProps { interface NotFoundPageProps {
@ -12,6 +12,8 @@ export const NotFoundPage = ({
}: Partial<NotFoundPageProps>) => { }: Partial<NotFoundPageProps>) => {
const error = useRouteError() as Partial<NotFoundPageProps> const error = useRouteError() as Partial<NotFoundPageProps>
const location = useLocation()
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='ContainerMain'> <div className='ContainerMain'>
@ -23,7 +25,19 @@ export const NotFoundPage = ({
<div> <div>
<p>{error?.message || message}</p> <p>{error?.message || message}</p>
</div> </div>
<div className='IBMSMAction'> <div
className='IBMSMAction'
style={{
gap: '10px'
}}
>
<Link
to={location.pathname}
className='btn btnMain IBMSMActionBtn'
type='button'
>
Try again
</Link>
<Link <Link
to={appRoutes.home} to={appRoutes.home}
className='btn btnMain IBMSMActionBtn' 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' } 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,10 +16,12 @@ import {
DEFAULT_FILTER_OPTIONS, DEFAULT_FILTER_OPTIONS,
extractBlogCardDetails, extractBlogCardDetails,
extractModData, extractModData,
getFallbackPubkey,
getLocalStorageItem, getLocalStorageItem,
getReportingSet, getReportingSet,
log, log,
LogType LogType,
timeout
} from 'utils' } from 'utils'
export const modRouteLoader = export const modRouteLoader =
@ -46,7 +48,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
@ -82,8 +85,8 @@ export const modRouteLoader =
// Parallel fetch mod event, latest events, mute, nsfw, repost lists // Parallel fetch mod event, latest events, mute, nsfw, repost lists
const settled = await Promise.allSettled([ const settled = await Promise.allSettled([
ndkContext.fetchEvent(modFilter), Promise.race([ndkContext.fetchEvent(modFilter), timeout(2000)]),
ndkContext.fetchEvents(latestFilter), Promise.race([ndkContext.fetchEvents(latestFilter), timeout(2000)]),
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
getReportingSet(CurationSetIdentifiers.NSFW, ndkContext), getReportingSet(CurationSetIdentifiers.NSFW, ndkContext),
getReportingSet(CurationSetIdentifiers.Repost, ndkContext) getReportingSet(CurationSetIdentifiers.Repost, ndkContext)

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

@ -705,3 +705,28 @@ a:hover {
transform: rotate(360deg); 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 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
}
}