Merge pull request 'Can now modify landing page. Fallback images for games and mods. Cached events.' (#29) from staging into master
All checks were successful
Release to Staging / build_and_release (push) Successful in 44s

Reviewed-on: #29
This commit is contained in:
freakoverse 2024-09-02 13:28:17 +00:00
commit 0e08d09717
21 changed files with 565 additions and 287 deletions

View File

@ -6,3 +6,9 @@ VITE_ADMIN_NPUBS= <A comma separated list of npubs>
# A dedicated npub used for reporting mods, blogs, profile and etc. # A dedicated npub used for reporting mods, blogs, profile and etc.
VITE_REPORTING_NPUB= <npub1...> VITE_REPORTING_NPUB= <npub1...>
# if there's no featured image, or if the image breaks somewhere down the line, then it should default to this image
VITE_FALLBACK_MOD_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png
# if there's no image, or if the image breaks somewhere down the line, then it should default to this image
VITE_FALLBACK_GAME_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png

View File

@ -25,6 +25,8 @@ jobs:
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
cat .env cat .env
- name: Create Build - name: Create Build

View File

@ -25,6 +25,8 @@ jobs:
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
cat .env cat .env
- name: Create Build - name: Create Build

View File

@ -6,7 +6,10 @@
<!-- Open Graph Meta Tags --> <!-- Open Graph Meta Tags -->
<meta property="og:title" content="DEG Mods - Liberating Game Mods" /> <meta property="og:title" content="DEG Mods - Liberating Game Mods" />
<meta property="og:description" content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely." /> <meta
property="og:description"
content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely."
/>
<meta property="og:image" content="/assets/img/DEGM%20Thumb.png" /> <meta property="og:image" content="/assets/img/DEGM%20Thumb.png" />
<meta property="og:url" content="https://degmods.com" /> <meta property="og:url" content="https://degmods.com" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
@ -14,24 +17,33 @@
<!-- Twitter Card Meta Tags --> <!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="DEG Mods - Liberating Game Mods" /> <meta name="twitter:title" content="DEG Mods - Liberating Game Mods" />
<meta name="twitter:description" content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely." /> <meta
name="twitter:description"
content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely."
/>
<meta name="twitter:image" content="/assets/img/DEGM%20Thumb.png" /> <meta name="twitter:image" content="/assets/img/DEGM%20Thumb.png" />
<!-- Other Meta Tags --> <!-- Other Meta Tags -->
<meta name="description" content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely." /> <meta
name="description"
content="Never get your game mods censored, get banned, lose your history, nor lose the connection between game mod creators and fans. Download your mods freely."
/>
<!-- Links and Stylesheets --> <!-- Links and Stylesheets -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/6.4.8/swiper-bundle.min.css" />
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css" /> <link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="/assets/fonts/fontawesome-all.min.css" /> <link rel="stylesheet" href="/assets/fonts/fontawesome-all.min.css" />
<link rel="icon" type="image/png" sizes="935x934" href="/assets/img/Logo%20with%20circle.png" /> <link
rel="icon"
type="image/png"
sizes="935x934"
href="/assets/img/Logo%20with%20circle.png"
/>
<title>DEG Mods - Liberating Game Mods</title> <title>DEG Mods - Liberating Game Mods</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Swiper/6.4.8/swiper-bundle.min.js"></script> <script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
</body> </body>
</html> </html>

19
package-lock.json generated
View File

@ -34,6 +34,7 @@
"react-router-dom": "^6.24.1", "react-router-dom": "^6.24.1",
"react-toastify": "10.0.5", "react-toastify": "10.0.5",
"react-window": "1.8.10", "react-window": "1.8.10",
"swiper": "11.1.11",
"uuid": "10.0.0", "uuid": "10.0.0",
"webln": "0.3.2" "webln": "0.3.2"
}, },
@ -4932,6 +4933,24 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/swiper": {
"version": "11.1.11",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.1.11.tgz",
"integrity": "sha512-077Aw3OrlZpkkBRf/6+44bGh/HZY/vsLEyate2db2KkJgYUIR5TvDgvvhcJtW/puXzw79w5KBc30DauEX6GZYQ==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

View File

@ -36,6 +36,7 @@
"react-router-dom": "^6.24.1", "react-router-dom": "^6.24.1",
"react-toastify": "10.0.5", "react-toastify": "10.0.5",
"react-window": "1.8.10", "react-window": "1.8.10",
"swiper": "11.1.11",
"uuid": "10.0.0", "uuid": "10.0.0",
"webln": "0.3.2" "webln": "0.3.2"
}, },

View File

@ -1,20 +1,23 @@
import '../styles/cardGames.css' import '../styles/cardGames.css'
import { handleGameImageError } from '../utils'
type GameCardProps = { type GameCardProps = {
backgroundLink: string title: string
imageUrl: string
} }
export const GameCard = ({ backgroundLink }: GameCardProps) => { export const GameCard = ({ title, imageUrl }: GameCardProps) => {
return ( return (
<a className='cardGameMainWrapperLink' href='search.html'> <a className='cardGameMainWrapperLink' href='search.html'>
<div <div className='cardGameMainWrapper'>
className='cardGameMain' <img
style={{ src={imageUrl}
background: `url("${backgroundLink}") center / cover no-repeat` onError={handleGameImageError}
}} className='cardGameMain'
></div> />
</div>
<div className='cardGameMainTitle'> <div className='cardGameMainTitle'>
<p>This is a game title, the best game title</p> <p>{title}</p>
</div> </div>
</a> </a>
) )

View File

@ -1,9 +1,10 @@
import '../styles/cardMod.css' import '../styles/cardMod.css'
import { handleModImageError } from '../utils'
type ModCardProps = { type ModCardProps = {
title: string title: string
summary: string summary: string
backgroundLink: string imageUrl: string
link: string link: string
handleClick: () => void handleClick: () => void
} }
@ -11,7 +12,7 @@ type ModCardProps = {
export const ModCard = ({ export const ModCard = ({
title, title,
summary, summary,
backgroundLink, imageUrl,
link, link,
handleClick handleClick
}: ModCardProps) => { }: ModCardProps) => {
@ -25,12 +26,13 @@ export const ModCard = ({
}} }}
> >
<div className='cardModMain'> <div className='cardModMain'>
<div <div className='cMMPictureWrapper'>
className='cMMPicture' <img
style={{ src={imageUrl}
background: `url("${backgroundLink}") center / cover no-repeat` onError={handleModImageError}
}} className='cMMPicture'
></div> />
</div>
<div className='cMMBody'> <div className='cMMBody'>
<h3 className='cMMBodyTitle'>{title}</h3> <h3 className='cMMBodyTitle'>{title}</h3>
<p className='cMMBodyText'>{summary}</p> <p className='cMMBodyText'>{summary}</p>

View File

@ -1,2 +1,38 @@
export const T_TAG_VALUE = 'GameMod' export const T_TAG_VALUE = 'GameMod'
export const MOD_FILTER_LIMIT = 20 export const MOD_FILTER_LIMIT = 20
export const LANDING_PAGE_DATA = {
featuredSlider: [
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5cek8pnrwc34xgknyv33xqkngc34xyknscfjxsknzvp38quxgc33vejnqvqhqecq8',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj'
],
awesomeMods: [
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5df5xccngvtrxqkkydpexukngvp4xgknsvp4vskkgdrxvgmkxdmp8quxycgx78rpf',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vrrvgmnjc33xuknwde4vskngvekxgknsenyxvkk2ctxvscrvenpvsmnxeqydygjx'
],
featuredGames: [
{
title: 'SUPERHOT',
imageUrl: ''
},
{
title: 'The Bounce House',
imageUrl: ''
},
{
title: 'Immortal Guns',
imageUrl: ''
},
{
title: 'Magenta Horizon Act 1',
imageUrl: ''
},
{
title: 'DEAD LETTER DEPT. Demo',
imageUrl: ''
}
]
}

View File

@ -14,6 +14,7 @@ import { MetadataController, UserRelaysType } from './metadata'
*/ */
export class RelayController { export class RelayController {
private static instance: RelayController private static instance: RelayController
private events = new Map<string, Event>()
private debug = true private debug = true
public connectedRelays: Relay[] = [] public connectedRelays: Relay[] = []
@ -151,6 +152,16 @@ export class RelayController {
// Wait for all publish operations to complete (either fulfilled or rejected) // Wait for all publish operations to complete (either fulfilled or rejected)
await Promise.allSettled(publishPromises) await Promise.allSettled(publishPromises)
if (publishedOnRelays.length > 0) {
// if the event was successfully published to relays then check if it contains the `aTag`
// if so, then cache the event
const aTag = event.tags.find((item) => item[0] === 'a')
if (aTag && aTag[1]) {
this.events.set(aTag[1], event)
}
}
// Return the list of relay URLs where the event was published // Return the list of relay URLs where the event was published
return publishedOnRelays return publishedOnRelays
} }
@ -335,13 +346,35 @@ export class RelayController {
filter: Filter, filter: Filter,
relays: string[] = [] relays: string[] = []
): Promise<Event | null> => { ): Promise<Event | null> => {
// first check if event is present in cached map then return that
// otherwise query relays
if (filter['#a']) {
const aTag = filter['#a'][0]
const cachedEvent = this.events.get(aTag)
if (cachedEvent) return cachedEvent
}
const events = await this.fetchEvents(filter, relays) const events = await this.fetchEvents(filter, relays)
// Sort events by creation date in descending order // Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at) events.sort((a, b) => b.created_at - a.created_at)
// Return the most recent event, or null if no events were received if (events.length > 0) {
return events[0] || null const event = events[0]
// if the aTag was specified in filter then cache the fetched event before returning
if (filter['#a']) {
const aTag = filter['#a'][0]
this.events.set(aTag, event)
}
// return the event
return event
}
// return null if event array is empty
return null
} }
/** /**
@ -358,6 +391,15 @@ export class RelayController {
hexKey: string, hexKey: string,
userRelaysType: UserRelaysType userRelaysType: UserRelaysType
) => { ) => {
// first check if event is present in cached map then return that
// otherwise query relays
if (filter['#a']) {
const aTag = filter['#a'][0]
const cachedEvent = this.events.get(aTag)
if (cachedEvent) return cachedEvent
}
// Get an instance of the MetadataController, which manages user metadata and relays // Get an instance of the MetadataController, which manages user metadata and relays
const metadataController = await MetadataController.getInstance() const metadataController = await MetadataController.getInstance()

View File

@ -35,11 +35,26 @@ export const GamesPage = () => {
</div> </div>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList IBMSMListFeaturedAlt'> <div className='IBMSMList IBMSMListFeaturedAlt'>
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' /> <GameCard
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' /> title='This is a game title, the best game title'
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' /> imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' /> />
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' /> <GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
<GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
<GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
<GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
</div> </div>
</div> </div>
<div className='IBMSecMain'> <div className='IBMSecMain'>

View File

@ -1,123 +1,58 @@
import { Filter, kinds, nip19 } from 'nostr-tools'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { A11y, Navigation, Pagination } from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/react'
import { BlogCard } from '../components/BlogCard' import { BlogCard } from '../components/BlogCard'
import { GameCard } from '../components/GameCard' import { GameCard } from '../components/GameCard'
import { ModCard } from '../components/ModCard' import { ModCard } from '../components/ModCard'
import { LANDING_PAGE_DATA } from '../constants'
import { RelayController } from '../controllers'
import { useDidMount } from '../hooks'
import { appRoutes, getModsInnerPageRoute } from '../routes'
import { ModDetails } from '../types'
import {
extractModData,
fetchMods,
handleModImageError,
log,
LogType
} from '../utils'
import '../styles/cardLists.css' import '../styles/cardLists.css'
import '../styles/SimpleSlider.css' import '../styles/SimpleSlider.css'
import '../styles/styles.css' import '../styles/styles.css'
// Import Swiper styles
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
export const HomePage = () => { export const HomePage = () => {
const navigate = useNavigate()
return ( return (
<div className='InnerBodyMain'> <div className='InnerBodyMain'>
<div className='SliderWrapper'> <div className='SliderWrapper'>
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='simple-slider IBMSMSlider'> <div className='simple-slider IBMSMSlider'>
<div className='swiper-container IBMSMSliderContainer'> <Swiper
<div className='swiper-wrapper IBMSMSliderContainerWrapper'> className='swiper-container IBMSMSliderContainer'
<div className='swiper-slide IBMSMSliderContainerWrapperSlider'> wrapperClass='swiper-wrapper IBMSMSliderContainerWrapper'
<div modules={[Navigation, Pagination, A11y]}
className='IBMSMSCWSPic' pagination={{ clickable: true, dynamicBullets: true }}
style={{ slidesPerView={1}
background: autoplay={{ delay: 5000 }}
'url("/assets/img/DEGMods%20Placeholder%20Img.png") center / cover no-repeat' speed={1000}
}} navigation
></div> loop
<div className='IBMSMSCWSInfo'> >
<h3 className='IBMSMSCWSInfoHeading'>Placeholder</h3> {LANDING_PAGE_DATA.featuredSlider.map((naddr) => (
<p className='IBMSMSCWSInfoText'> <SwiperSlide className='swiper-slide IBMSMSliderContainerWrapperSlider'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. <SlideContent naddr={naddr} />
Integer nec odio. Praesent libero. Sed cursus ante </SwiperSlide>
dapibus diam. Sed nisi. Nulla quis sem at nibh elementum ))}
imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce </Swiper>
nec tellus sed augue semper porta. Mauris massa.
Vestibulum lacinia arcu eget nulla. className aptent
taciti sociosqu ad litora torquent per conubia nostra,
per inceptos himenaeos. Curabitur sodales ligula in
libero.
<br />
</p>
<div className='IBMSMSliderContainerWrapperSliderAction'>
<a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
role='button'
href='mods-inner.html'
>
Check it out
</a>
</div>
</div>
</div>
<div className='swiper-slide IBMSMSliderContainerWrapperSlider'>
<div
className='IBMSMSCWSPic'
style={{
background:
'url("/assets/img/DEGMods%20Placeholder%20Img.png") center / cover no-repeat'
}}
></div>
<div className='IBMSMSCWSInfo'>
<h3 className='IBMSMSCWSInfoHeading'>Placeholder</h3>
<p className='IBMSMSCWSInfoText'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Integer nec odio. Praesent libero. Sed cursus ante
dapibus diam. Sed nisi. Nulla quis sem at nibh elementum
imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce
nec tellus sed augue semper porta. Mauris massa.
Vestibulum lacinia arcu eget nulla. className aptent
taciti sociosqu ad litora torquent per conubia nostra,
per inceptos himenaeos. Curabitur sodales ligula in
libero.
<br />
</p>
<div className='IBMSMSliderContainerWrapperSliderAction'>
<a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
role='button'
href='mods-inner.html'
>
Check it out
</a>
</div>
</div>
</div>
<div className='swiper-slide IBMSMSliderContainerWrapperSlider'>
<div
className='IBMSMSCWSPic'
style={{
background:
'url("/assets/img/DEGMods%20Placeholder%20Img.png") center / cover no-repeat'
}}
></div>
<div className='IBMSMSCWSInfo'>
<h3 className='IBMSMSCWSInfoHeading'>Placeholder</h3>
<p className='IBMSMSCWSInfoText'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Integer nec odio. Praesent libero. Sed cursus ante
dapibus diam. Sed nisi. Nulla quis sem at nibh elementum
imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce
nec tellus sed augue semper porta. Mauris massa.
Vestibulum lacinia arcu eget nulla. className aptent
taciti sociosqu ad litora torquent per conubia nostra,
per inceptos himenaeos. Curabitur sodales ligula in
libero.
<br />
</p>
<div className='IBMSMSliderContainerWrapperSliderAction'>
<a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
role='button'
href='mods-inner.html'
>
Check it out
</a>
</div>
</div>
</div>
</div>
<div className='swiper-pagination'></div>
<div className='swiper-button-prev'></div>
<div className='swiper-button-next'></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -126,20 +61,18 @@ export const HomePage = () => {
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Cool Games (WIP)</h2> <h2 className='IBMSMTitleMainHeading'>Cool Games</h2>
</div> </div>
<div className='IBMSMList IBMSMListFeaturedAlt'> <div className='IBMSMList IBMSMListFeaturedAlt'>
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' /> {LANDING_PAGE_DATA.featuredGames.map((game) => (
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' /> <GameCard title={game.title} imageUrl={game.imageUrl} />
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' /> ))}
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
</div> </div>
<div className='IBMSMAction'> <div className='IBMSMAction'>
<a <a
className='btn btnMain IBMSMActionBtn' className='btn btnMain IBMSMActionBtn'
role='button' role='button'
href='blog.html' onClick={() => navigate(appRoutes.games)}
> >
View All View All
</a> </a>
@ -147,114 +80,24 @@ export const HomePage = () => {
</div> </div>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Awesome Mods (WIP)</h2> <h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
</div> </div>
<div className='IBMSMList IBMSMListAlt'> <div className='IBMSMList IBMSMListAlt'>
<ModCard {LANDING_PAGE_DATA.awesomeMods.map((naddr) => (
title='Placeholder' <DisplayMod key={naddr} naddr={naddr} />
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.' ))}
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
</div> </div>
<div className='IBMSMAction'> <div className='IBMSMAction'>
<a <a
className='btn btnMain IBMSMActionBtn' className='btn btnMain IBMSMActionBtn'
role='button' role='button'
href='blog.html' onClick={() => navigate(appRoutes.mods)}
>
View All
</a>
</div>
</div>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Latest Mods (WIP)</h2>
</div>
<div className='IBMSMList'>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
<ModCard
title='Placeholder'
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
link=''
handleClick={() => {
alert(
'these are dummy mods. So navigation on these are not implemented yet'
)
}}
/>
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
href='blog.html'
> >
View All View All
</a> </a>
</div> </div>
</div> </div>
<DisplayLatestMods />
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Blog Posts (WIP)</h2> <h2 className='IBMSMTitleMainHeading'>Blog Posts (WIP)</h2>
@ -281,3 +124,189 @@ export const HomePage = () => {
</div> </div>
) )
} }
type SlideContentProps = {
naddr: string
}
const SlideContent = ({ naddr }: SlideContentProps) => {
const navigate = useNavigate()
const [mod, setMod] = useState<ModDetails>()
useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { identifier, kind, pubkey, relays = [] } = decoded.data
const filter: Filter = {
'#a': [identifier],
authors: [pubkey],
kinds: [kind]
}
RelayController.getInstance()
.fetchEvent(filter, relays)
.then((event) => {
if (event) {
const extracted = extractModData(event)
setMod(extracted)
}
})
.catch((err) => {
log(
true,
LogType.Error,
'An error occurred in fetching mod details from relays',
err
)
})
})
if (!mod) return <Spinner />
return (
<>
<div className='IBMSMSCWSPicWrapper'>
<img
src={mod.featuredImageUrl}
onError={handleModImageError}
className='IBMSMSCWSPic'
/>
</div>
<div className='IBMSMSCWSInfo'>
<h3 className='IBMSMSCWSInfoHeading'>{mod.title}</h3>
<p className='IBMSMSCWSInfoText'>
{mod.summary}
<br />
</p>
<div className='IBMSMSliderContainerWrapperSliderAction'>
<a
className='btn btnMain IBMSMSliderContainerWrapperSliderActionbtn'
role='button'
onClick={() => navigate(getModsInnerPageRoute(naddr))}
>
Check it out
</a>
</div>
</div>
</>
)
}
type DisplayModProps = {
naddr: string
}
const DisplayMod = ({ naddr }: DisplayModProps) => {
const navigate = useNavigate()
const [mod, setMod] = useState<ModDetails>()
useDidMount(() => {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
const { identifier, kind, pubkey, relays = [] } = decoded.data
const filter: Filter = {
'#a': [identifier],
authors: [pubkey],
kinds: [kind]
}
RelayController.getInstance()
.fetchEvent(filter, relays)
.then((event) => {
if (event) {
const extracted = extractModData(event)
setMod(extracted)
}
})
.catch((err) => {
log(
true,
LogType.Error,
'An error occurred in fetching mod details from relays',
err
)
})
})
if (!mod) return <Spinner />
const route = getModsInnerPageRoute(naddr)
return (
<ModCard
title={mod.title}
summary={mod.summary}
imageUrl={mod.featuredImageUrl}
link={`#${route}`}
handleClick={() => navigate(route)}
/>
)
}
const DisplayLatestMods = () => {
const navigate = useNavigate()
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
useDidMount(() => {
fetchMods({ source: window.location.host, limit: 4 })
.then((res) => {
setLatestMods(res)
})
.finally(() => {
setIsFetchingLatestMods(false)
})
})
return (
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Latest Mods</h2>
</div>
<div className='IBMSMList'>
{isFetchingLatestMods ? (
<Spinner />
) : (
latestMods.map((mod) => {
const route = getModsInnerPageRoute(
nip19.naddrEncode({
identifier: mod.aTag,
pubkey: mod.author,
kind: kinds.ClassifiedListing
})
)
return (
<ModCard
key={mod.id}
title={mod.title}
summary={mod.summary}
imageUrl={mod.featuredImageUrl}
link={`#${route}`}
handleClick={() => navigate(route)}
/>
)
})
)}
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
onClick={() => navigate(appRoutes.mods)}
>
View All
</a>
</div>
</div>
)
}
const Spinner = () => {
return (
<div className='spinner'>
<div className='spinnerCircle'></div>
</div>
)
}

View File

@ -91,7 +91,7 @@ export const ModsPage = () => {
useEffect(() => { useEffect(() => {
setIsFetching(true) setIsFetching(true)
fetchMods(filterOptions.source) fetchMods({ source: filterOptions.source })
.then((res) => { .then((res) => {
setMods(res) setMods(res)
}) })
@ -106,7 +106,10 @@ export const ModsPage = () => {
const until = const until =
mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined
fetchMods(filterOptions.source, until) fetchMods({
source: filterOptions.source,
until
})
.then((res) => { .then((res) => {
setMods(res) setMods(res)
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
@ -121,7 +124,10 @@ export const ModsPage = () => {
const since = mods.length > 0 ? mods[0].published_at + 1 : undefined const since = mods.length > 0 ? mods[0].published_at + 1 : undefined
fetchMods(filterOptions.source, undefined, since) fetchMods({
source: filterOptions.source,
since
})
.then((res) => { .then((res) => {
setMods(res) setMods(res)
setPage((prev) => prev - 1) setPage((prev) => prev - 1)
@ -215,7 +221,7 @@ export const ModsPage = () => {
key={mod.id} key={mod.id}
title={mod.title} title={mod.title}
summary={mod.summary} summary={mod.summary}
backgroundLink={mod.featuredImageUrl} imageUrl={mod.featuredImageUrl}
link={`#${route}`} link={`#${route}`}
handleClick={() => navigate(route)} handleClick={() => navigate(route)}
/> />

View File

@ -1,6 +1,6 @@
.swiper-pagination-bullet-active { .swiper-pagination-bullet-active {
background: rgba(255,255,255,0.5); background: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 4px 0 rgba(0,0,0,0.5); box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.5);
} }
.simple-slider .swiper-slide { .simple-slider .swiper-slide {
@ -22,16 +22,22 @@
} }
} }
.simple-slider .swiper-button-next, .simple-slider .swiper-button-prev { .simple-slider .swiper-button-next,
.simple-slider .swiper-button-prev {
width: 50px; width: 50px;
margin-left: 00px; margin-left: 00px;
margin-right: 00px; margin-right: 00px;
color: rgba(255,255,255,0.5); color: rgba(255, 255, 255, 0.5);
background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.05)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); background: linear-gradient(
rgba(255, 255, 255, 0.05),
rgba(255, 255, 255, 0.05)
),
linear-gradient(to top right, #262626, #292929, #262626),
linear-gradient(to top right, #262626, #292929, #262626);
padding: 10px; padding: 10px;
height: 75px; height: 75px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -39,19 +45,27 @@
margin-top: -35px; margin-top: -35px;
} }
.simple-slider .swiper-button-next:hover, .simple-slider .swiper-button-prev:hover { .simple-slider .swiper-button-next:hover,
background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.1)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); .simple-slider .swiper-button-prev:hover {
background: linear-gradient(
rgba(255, 255, 255, 0.1),
rgba(255, 255, 255, 0.1)
),
linear-gradient(to top right, #262626, #292929, #262626),
linear-gradient(to top right, #262626, #292929, #262626);
} }
.swiper-button-next:after, .swiper-button-prev:after { .swiper-button-next:after,
font-size: 18px; .swiper-button-prev:after {
font-size: 18px!important;
} }
@media (max-width:992px) { @media (max-width: 992px) {
.simple-slider .swiper-button-next, .simple-slider .swiper-button-prev { .simple-slider .swiper-button-next,
.simple-slider .swiper-button-prev {
bottom: 0; bottom: 0;
top: unset; top: unset;
width: 48%; width: 45%;
height: unset; height: unset;
padding: 10px; padding: 10px;
} }
@ -115,7 +129,12 @@
bottom: 0; bottom: 0;
right: 0; right: 0;
left: 0; left: 0;
background: linear-gradient(rgba(255,255,255,0.15), rgba(255,255,255,0.15)), linear-gradient(to top right, #262626, #292929, #262626), linear-gradient(to top right, #262626, #292929, #262626); background: linear-gradient(
rgba(255, 255, 255, 0.15),
rgba(255, 255, 255, 0.15)
),
linear-gradient(to top right, #262626, #292929, #262626),
linear-gradient(to top right, #262626, #292929, #262626);
z-index: -1; z-index: -1;
border-radius: 10px; border-radius: 10px;
} }
@ -129,12 +148,15 @@
opacity: 1; opacity: 1;
} }
.swiper-container-horizontal > .swiper-pagination-bullets, .swiper-pagination-custom, .swiper-pagination-fraction { .swiper-container-horizontal > .swiper-pagination-bullets,
.swiper-pagination-custom,
.swiper-pagination-fraction {
width: 100%; width: 100%;
bottom: 0; bottom: 0;
} }
.swiper-button-next, .swiper-button-prev { .swiper-button-next,
.swiper-button-prev {
position: absolute; position: absolute;
} }
@ -161,23 +183,35 @@
.SliderWrapper { .SliderWrapper {
width: 100%; width: 100%;
padding: 50px 0; padding: 50px 0;
background: rgba(0,0,0,0.1); background: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin: -25px 0 0 0; margin: -25px 0 0 0;
border-bottom: solid 1px rgba(255,255,255,0.05); border-bottom: solid 1px rgba(255, 255, 255, 0.05);
} }
.IBMSMSCWSPic { .IBMSMSCWSPic {
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
border: solid 1px rgba(255,255,255,0.05); border: solid 1px rgba(255, 255, 255, 0.05);
padding-top: 50%;
z-index: 1; z-index: 1;
box-shadow: 0 0 8px 0 rgba(0,0,0,0.25); box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25);
width: 100%;
height: 100%;
object-fit: cover; /* Ensures the image covers the container like a background image */
}
.IBMSMSCWSPicWrapper {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
} }
.IBMSMSCWSInfo { .IBMSMSCWSInfo {
@ -187,9 +221,15 @@
justify-content: center; justify-content: center;
padding: 25px; padding: 25px;
border-radius: 10px; border-radius: 10px;
background: linear-gradient(rgba(255,255,255,0), rgba(255,255,255,0)), linear-gradient(to top right, rgb(38,38,38), rgb(41,41,41), rgb(38,38,38)); background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0)),
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); linear-gradient(
border: solid 1px rgba(255,255,255,0.05); to top right,
rgb(38, 38, 38),
rgb(41, 41, 41),
rgb(38, 38, 38)
);
box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
border: solid 1px rgba(255, 255, 255, 0.05);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -212,7 +252,7 @@
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
font-size: 20px; font-size: 20px;
line-height: 1.25; line-height: 1.25;
color: rgba(255,255,255,0.75); color: rgba(255, 255, 255, 0.75);
font-weight: bold; font-weight: bold;
} }
@ -221,7 +261,7 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
-webkit-line-clamp: 8; -webkit-line-clamp: 8;
color: rgba(255,255,255,0.5); color: rgba(255, 255, 255, 0.5);
font-size: 15px; font-size: 15px;
line-height: 1.5; line-height: 1.5;
flex-grow: 1; flex-grow: 1;
@ -235,6 +275,8 @@
} }
.swiper-pagination { .swiper-pagination {
display: none;
bottom: -10px !important;
} }
@media (max-width: 992px) { @media (max-width: 992px) {
@ -244,7 +286,7 @@
} }
.swiper-pagination-bullet { .swiper-pagination-bullet {
background: rgba(0,0,0,0.5); background: rgba(0, 0, 0, 0.5);
opacity: 1; opacity: 1;
width: 12px; width: 12px;
height: 12px; height: 12px;
@ -252,6 +294,5 @@
} }
.swiper-pagination-bullet.swiper-pagination-bullet-active { .swiper-pagination-bullet.swiper-pagination-bullet-active {
background: rgba(128,0,255,0.5); background: rgba(128, 0, 255, 0.5);
} }

View File

@ -17,16 +17,24 @@
transform: scale(1); transform: scale(1);
} }
.cardGameMain { .cardGameMainWrapper {
position: relative;
padding-top: 150%; padding-top: 150%;
}
.cardGameMain {
border-radius: 15px; border-radius: 15px;
background: rgba(255,255,255,0.05); box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1); width: 100%;
object-fit: cover; /* Ensures the image covers the container like a background image */
position: absolute;
height: 100%;
top: 0;
} }
.cardGameMainTitle { .cardGameMainTitle {
transition: ease 0.4s; transition: ease 0.4s;
color: rgba(255,255,255,0.5); color: rgba(255, 255, 255, 0.5);
padding: 0 15px; padding: 0 15px;
font-weight: bold; font-weight: bold;
display: -webkit-box; display: -webkit-box;
@ -36,4 +44,3 @@
font-size: 18px; font-size: 18px;
line-height: 1.5; line-height: 1.5;
} }

View File

@ -9,11 +9,18 @@
background: linear-gradient(to top right, #262626, #292929, #262626); background: linear-gradient(to top right, #262626, #292929, #262626);
} }
.cMMPicture { .cMMPictureWrapper {
position: relative; position: relative;
width: 100%; width: 100%;
padding-top: 56.25%; padding-top: 56.25%;
background: rgba(0, 0, 0, 0.1); }
.cMMPicture {
position: absolute;
width: 100%;
height: 100%;
top: 0;
object-fit: cover; /* Ensures the image covers the container like a background image */
} }
.cMMBody { .cMMBody {

View File

@ -655,3 +655,28 @@ a:hover {
.errorMainText { .errorMainText {
} }
.spinner {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.spinnerCircle {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -27,6 +27,7 @@
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1); box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
text-decoration: unset;
} }
.IBMSMSMBSSTagsTag:active { .IBMSMSMBSSTagsTag:active {

View File

@ -134,6 +134,13 @@ export const initializeFormState = (
] ]
}) })
interface FetchModsOptions {
source?: string
until?: number
since?: number
limit?: number
}
/** /**
* Fetches a list of mods based on the provided source. * Fetches a list of mods based on the provided source.
* *
@ -144,15 +151,16 @@ export const initializeFormState = (
* @returns A promise that resolves to an array of `ModDetails` objects. In case of an error, * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error,
* it logs the error and shows a notification, then returns an empty array. * it logs the error and shows a notification, then returns an empty array.
*/ */
export const fetchMods = async ( export const fetchMods = async ({
source: string, source,
until?: number, until,
since?: number since,
): Promise<ModDetails[]> => { limit
}: FetchModsOptions): Promise<ModDetails[]> => {
// Define the filter criteria for fetching mods // Define the filter criteria for fetching mods
const filter: Filter = { const filter: Filter = {
kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
limit: MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
'#t': [T_TAG_VALUE], '#t': [T_TAG_VALUE],
until, // Optional filter to fetch events until this timestamp 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

View File

@ -123,3 +123,15 @@ export const abbreviateNumber = (value: number): string => {
return value.toString() return value.toString()
} }
} }
export const handleGameImageError = (
e: React.SyntheticEvent<HTMLImageElement, Event>
) => {
e.currentTarget.src = import.meta.env.VITE_FALLBACK_GAME_IMAGE
}
export const handleModImageError = (
e: React.SyntheticEvent<HTMLImageElement, Event>
) => {
e.currentTarget.src = import.meta.env.VITE_FALLBACK_MOD_IMAGE
}

2
src/vite-env.d.ts vendored
View File

@ -4,6 +4,8 @@ interface ImportMetaEnv {
readonly VITE_APP_RELAY: string readonly VITE_APP_RELAY: string
readonly VITE_ADMIN_NPUBS: string readonly VITE_ADMIN_NPUBS: string
readonly VITE_REPORTING_NPUB: string readonly VITE_REPORTING_NPUB: string
readonly VITE_FALLBACK_MOD_IMAGE: string
readonly VITE_FALLBACK_GAME_IMAGE: string
// more env variables... // more env variables...
} }