Can now modify landing page. Fallback images for games and mods. Cached events. #29

Merged
freakoverse merged 16 commits from staging into master 2024-09-02 13:28:18 +00:00
21 changed files with 565 additions and 287 deletions

View File

@ -5,4 +5,10 @@ VITE_APP_RELAY=wss://relay.degmods.com
VITE_ADMIN_NPUBS= <A comma separated list of npubs>
# 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_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .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
- name: Create Build

View File

@ -25,6 +25,8 @@ jobs:
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .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
- name: Create Build

View File

@ -6,7 +6,10 @@
<!-- Open Graph Meta Tags -->
<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:url" content="https://degmods.com" />
<meta property="og:type" content="website" />
@ -14,24 +17,33 @@
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<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" />
<!-- 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 -->
<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/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>
</head>
<body>
<div id="root"></div>
<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>
</html>

19
package-lock.json generated
View File

@ -34,6 +34,7 @@
"react-router-dom": "^6.24.1",
"react-toastify": "10.0.5",
"react-window": "1.8.10",
"swiper": "11.1.11",
"uuid": "10.0.0",
"webln": "0.3.2"
},
@ -4932,6 +4933,24 @@
"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": {
"version": "0.2.0",
"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-toastify": "10.0.5",
"react-window": "1.8.10",
"swiper": "11.1.11",
"uuid": "10.0.0",
"webln": "0.3.2"
},

View File

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

View File

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

View File

@ -1,2 +1,38 @@
export const T_TAG_VALUE = 'GameMod'
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 {
private static instance: RelayController
private events = new Map<string, Event>()
private debug = true
public connectedRelays: Relay[] = []
@ -151,6 +152,16 @@ export class RelayController {
// Wait for all publish operations to complete (either fulfilled or rejected)
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 publishedOnRelays
}
@ -335,13 +346,35 @@ export class RelayController {
filter: Filter,
relays: string[] = []
): 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)
// Sort events by creation date in descending order
events.sort((a, b) => b.created_at - a.created_at)
// Return the most recent event, or null if no events were received
return events[0] || null
if (events.length > 0) {
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,
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
const metadataController = await MetadataController.getInstance()

View File

@ -35,11 +35,26 @@ export const GamesPage = () => {
</div>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList IBMSMListFeaturedAlt'>
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='/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'
/>
<GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
</div>
</div>
<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 { GameCard } from '../components/GameCard'
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/SimpleSlider.css'
import '../styles/styles.css'
// Import Swiper styles
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
export const HomePage = () => {
const navigate = useNavigate()
return (
<div className='InnerBodyMain'>
<div className='SliderWrapper'>
<div className='ContainerMain'>
<div className='IBMSecMain'>
<div className='simple-slider IBMSMSlider'>
<div className='swiper-container IBMSMSliderContainer'>
<div className='swiper-wrapper IBMSMSliderContainerWrapper'>
<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 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>
<Swiper
className='swiper-container IBMSMSliderContainer'
wrapperClass='swiper-wrapper IBMSMSliderContainerWrapper'
modules={[Navigation, Pagination, A11y]}
pagination={{ clickable: true, dynamicBullets: true }}
slidesPerView={1}
autoplay={{ delay: 5000 }}
speed={1000}
navigation
loop
>
{LANDING_PAGE_DATA.featuredSlider.map((naddr) => (
<SwiperSlide className='swiper-slide IBMSMSliderContainerWrapperSlider'>
<SlideContent naddr={naddr} />
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
@ -126,20 +61,18 @@ export const HomePage = () => {
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Cool Games (WIP)</h2>
<h2 className='IBMSMTitleMainHeading'>Cool Games</h2>
</div>
<div className='IBMSMList IBMSMListFeaturedAlt'>
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='assets/img/DEGMods%20Placeholder%20Img.png' />
<GameCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
{LANDING_PAGE_DATA.featuredGames.map((game) => (
<GameCard title={game.title} imageUrl={game.imageUrl} />
))}
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
href='blog.html'
onClick={() => navigate(appRoutes.games)}
>
View All
</a>
@ -147,114 +80,24 @@ export const HomePage = () => {
</div>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Awesome Mods (WIP)</h2>
<h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
</div>
<div className='IBMSMList IBMSMListAlt'>
<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'
)
}}
/>
{LANDING_PAGE_DATA.awesomeMods.map((naddr) => (
<DisplayMod key={naddr} naddr={naddr} />
))}
</div>
<div className='IBMSMAction'>
<a
className='btn btnMain IBMSMActionBtn'
role='button'
href='blog.html'
>
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'
onClick={() => navigate(appRoutes.mods)}
>
View All
</a>
</div>
</div>
<DisplayLatestMods />
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Blog Posts (WIP)</h2>
@ -281,3 +124,189 @@ export const HomePage = () => {
</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(() => {
setIsFetching(true)
fetchMods(filterOptions.source)
fetchMods({ source: filterOptions.source })
.then((res) => {
setMods(res)
})
@ -106,7 +106,10 @@ export const ModsPage = () => {
const until =
mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined
fetchMods(filterOptions.source, until)
fetchMods({
source: filterOptions.source,
until
})
.then((res) => {
setMods(res)
setPage((prev) => prev + 1)
@ -121,7 +124,10 @@ export const ModsPage = () => {
const since = mods.length > 0 ? mods[0].published_at + 1 : undefined
fetchMods(filterOptions.source, undefined, since)
fetchMods({
source: filterOptions.source,
since
})
.then((res) => {
setMods(res)
setPage((prev) => prev - 1)
@ -215,7 +221,7 @@ export const ModsPage = () => {
key={mod.id}
title={mod.title}
summary={mod.summary}
backgroundLink={mod.featuredImageUrl}
imageUrl={mod.featuredImageUrl}
link={`#${route}`}
handleClick={() => navigate(route)}
/>

View File

@ -1,6 +1,6 @@
.swiper-pagination-bullet-active {
background: rgba(255,255,255,0.5);
box-shadow: 0 0 4px 0 rgba(0,0,0,0.5);
background: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.5);
}
.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;
margin-left: 00px;
margin-right: 00px;
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);
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);
padding: 10px;
height: 75px;
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;
flex-direction: column;
justify-content: center;
@ -39,19 +45,27 @@
margin-top: -35px;
}
.simple-slider .swiper-button-next:hover, .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);
.simple-slider .swiper-button-next:hover,
.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 {
font-size: 18px;
.swiper-button-next:after,
.swiper-button-prev:after {
font-size: 18px!important;
}
@media (max-width:992px) {
.simple-slider .swiper-button-next, .simple-slider .swiper-button-prev {
@media (max-width: 992px) {
.simple-slider .swiper-button-next,
.simple-slider .swiper-button-prev {
bottom: 0;
top: unset;
width: 48%;
width: 45%;
height: unset;
padding: 10px;
}
@ -115,7 +129,12 @@
bottom: 0;
right: 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;
border-radius: 10px;
}
@ -129,12 +148,15 @@
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%;
bottom: 0;
}
.swiper-button-next, .swiper-button-prev {
.swiper-button-next,
.swiper-button-prev {
position: absolute;
}
@ -161,23 +183,35 @@
.SliderWrapper {
width: 100%;
padding: 50px 0;
background: rgba(0,0,0,0.1);
background: rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
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 {
border-radius: 10px;
overflow: hidden;
border: solid 1px rgba(255,255,255,0.05);
padding-top: 50%;
border: solid 1px rgba(255, 255, 255, 0.05);
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 {
@ -187,9 +221,15 @@
justify-content: center;
padding: 25px;
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));
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
border: solid 1px rgba(255,255,255,0.05);
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)
);
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) {
@ -212,7 +252,7 @@
-webkit-line-clamp: 2;
font-size: 20px;
line-height: 1.25;
color: rgba(255,255,255,0.75);
color: rgba(255, 255, 255, 0.75);
font-weight: bold;
}
@ -221,7 +261,7 @@
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 8;
color: rgba(255,255,255,0.5);
color: rgba(255, 255, 255, 0.5);
font-size: 15px;
line-height: 1.5;
flex-grow: 1;
@ -235,6 +275,8 @@
}
.swiper-pagination {
display: none;
bottom: -10px !important;
}
@media (max-width: 992px) {
@ -244,7 +286,7 @@
}
.swiper-pagination-bullet {
background: rgba(0,0,0,0.5);
background: rgba(0, 0, 0, 0.5);
opacity: 1;
width: 12px;
height: 12px;
@ -252,6 +294,5 @@
}
.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);
}
.cardGameMain {
.cardGameMainWrapper {
position: relative;
padding-top: 150%;
}
.cardGameMain {
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 {
transition: ease 0.4s;
color: rgba(255,255,255,0.5);
color: rgba(255, 255, 255, 0.5);
padding: 0 15px;
font-weight: bold;
display: -webkit-box;
@ -36,4 +44,3 @@
font-size: 18px;
line-height: 1.5;
}

View File

@ -9,11 +9,18 @@
background: linear-gradient(to top right, #262626, #292929, #262626);
}
.cMMPicture {
.cMMPictureWrapper {
position: relative;
width: 100%;
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 {

View File

@ -655,3 +655,28 @@ a:hover {
.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);
box-shadow: 0 0 8px 0 rgb(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.05);
text-decoration: unset;
}
.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.
*
@ -144,15 +151,16 @@ export const initializeFormState = (
* @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.
*/
export const fetchMods = async (
source: string,
until?: number,
since?: number
): Promise<ModDetails[]> => {
export const fetchMods = async ({
source,
until,
since,
limit
}: FetchModsOptions): Promise<ModDetails[]> => {
// Define the filter criteria for fetching mods
const filter: Filter = {
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],
until, // Optional filter to fetch events until 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()
}
}
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_ADMIN_NPUBS: string
readonly VITE_REPORTING_NPUB: string
readonly VITE_FALLBACK_MOD_IMAGE: string
readonly VITE_FALLBACK_GAME_IMAGE: string
// more env variables...
}