feat: implemented mod details page #3
@ -1 +1,4 @@
|
||||
VITE_APP_RELAY=wss://relay.degmods.com
|
||||
# This relay will be used to publish/retrieve events along with other relays (user's relays, admin relays)
|
||||
VITE_APP_RELAY=wss://relay.degmods.com
|
||||
# A comma separated list of npubs, Relay list will be extracted for these npubs and this relay list will be used to publish event
|
||||
VITE_ADMIN_NPUBS= <A comma separated list of npubs>
|
@ -1,10 +1,12 @@
|
||||
import '../styles/cardMod.css'
|
||||
|
||||
type ModCardProps = {
|
||||
title: string
|
||||
summary: string
|
||||
backgroundLink: string
|
||||
}
|
||||
|
||||
export const ModCard = ({ backgroundLink }: ModCardProps) => {
|
||||
export const ModCard = ({ title, summary, backgroundLink }: ModCardProps) => {
|
||||
return (
|
||||
<a className='cardModMainWrapperLink' href='mods-inner.html'>
|
||||
<div className='cardModMain'>
|
||||
@ -15,21 +17,8 @@ export const ModCard = ({ backgroundLink }: ModCardProps) => {
|
||||
}}
|
||||
></div>
|
||||
<div className='cMMBody'>
|
||||
<h3 className='cMMBodyTitle'>
|
||||
This is a mod title for an awesome game that will make everyone
|
||||
happy! The happiest!
|
||||
</h3>
|
||||
<p className='cMMBodyText'>
|
||||
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. Class aptent taciti sociosqu ad
|
||||
litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
Curabitur sodales ligula in libero.
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
<h3 className='cMMBodyTitle'>{title}</h3>
|
||||
<p className='cMMBodyText'>{summary}</p>
|
||||
</div>
|
||||
<div className='cMMFoot'>
|
||||
<div className='cMMFootReactions'>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import NDK, { NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
|
||||
import { UserProfile } from '../types/user'
|
||||
import { hexToNpub } from '../utils'
|
||||
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||
|
||||
/**
|
||||
* Singleton class to manage metadata operations using NDK.
|
||||
@ -8,12 +8,42 @@ import { hexToNpub } from '../utils'
|
||||
export class MetadataController {
|
||||
private static instance: MetadataController
|
||||
private profileNdk: NDK
|
||||
public adminNpubs: string[]
|
||||
public adminRelays = new Set<string>()
|
||||
|
||||
private constructor() {
|
||||
this.profileNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://user.kindpag.es', 'wss://purplepag.es']
|
||||
})
|
||||
this.profileNdk.connect()
|
||||
|
||||
this.adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
|
||||
}
|
||||
|
||||
private setAdminRelays = async () => {
|
||||
const promises = this.adminNpubs.map((npub) => {
|
||||
const hexKey = npubToHex(npub)
|
||||
if (!hexKey) return null
|
||||
|
||||
return NDKRelayList.forUser(hexKey, this.profileNdk)
|
||||
.then((ndkRelayList) => {
|
||||
if (ndkRelayList) {
|
||||
ndkRelayList.writeRelayUrls.forEach((url) =>
|
||||
this.adminRelays.add(url)
|
||||
)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
|
||||
err
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.allSettled(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21,9 +51,11 @@ export class MetadataController {
|
||||
*
|
||||
* @returns The singleton instance of MetadataController.
|
||||
*/
|
||||
public static getInstance(): MetadataController {
|
||||
public static async getInstance(): Promise<MetadataController> {
|
||||
if (!MetadataController.instance) {
|
||||
MetadataController.instance = new MetadataController()
|
||||
|
||||
MetadataController.instance.setAdminRelays()
|
||||
}
|
||||
return MetadataController.instance
|
||||
}
|
||||
|
@ -68,11 +68,11 @@ export class RelayController {
|
||||
|
||||
// todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done
|
||||
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
|
||||
// Retrieve the list of write relays for the event's public key
|
||||
// Use a timeout to handle cases where retrieving write relays takes too long
|
||||
const writeRelaysPromise = MetadataController.getInstance().findWriteRelays(
|
||||
event.pubkey
|
||||
)
|
||||
const writeRelaysPromise = metadataController.findWriteRelays(event.pubkey)
|
||||
|
||||
log(this.debug, LogType.Info, `ℹ Finding user's write relays`)
|
||||
|
||||
@ -85,6 +85,11 @@ export class RelayController {
|
||||
return [] as string[] // Return an empty array if an error occurs
|
||||
})
|
||||
|
||||
// push admin relay urls obtained from metadata controller to writeRelayUrls list
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
writeRelayUrls.push(url)
|
||||
})
|
||||
|
||||
// Connect to all write relays obtained from MetadataController
|
||||
const relayPromises = writeRelayUrls.map((relayUrl) =>
|
||||
this.connectRelay(relayUrl)
|
||||
@ -141,17 +146,26 @@ export class RelayController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
||||
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
||||
* If no relays are specified, it defaults to using connected relays.
|
||||
*
|
||||
* @param {Filter} filter - The filter criteria to find the event.
|
||||
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
||||
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
||||
*/
|
||||
fetchEvent = async (
|
||||
fetchEvents = async (
|
||||
filter: Filter,
|
||||
relays: string[] = []
|
||||
): Promise<Event | null> => {
|
||||
): Promise<Event[]> => {
|
||||
// add app relay to relays array
|
||||
relays.push(import.meta.env.VITE_APP_RELAY)
|
||||
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
// add admin relays to relays array
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
relays.push(url)
|
||||
})
|
||||
|
||||
// Connect to all specified relays
|
||||
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
|
||||
await Promise.allSettled(relayPromises)
|
||||
@ -163,6 +177,7 @@ export class RelayController {
|
||||
}
|
||||
|
||||
const events: Event[] = []
|
||||
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
||||
|
||||
// Create a promise for each relay subscription
|
||||
const subPromises = this.connectedRelays.map((relay) => {
|
||||
@ -171,8 +186,17 @@ export class RelayController {
|
||||
const sub = relay.subscribe([filter], {
|
||||
// Handle incoming events
|
||||
onevent: (e) => {
|
||||
log(this.debug, LogType.Info, `ℹ ${relay.url} : Received Event`, e)
|
||||
events.push(e)
|
||||
// Add the event to the array if it's not a duplicate
|
||||
if (!eventIds.has(e.id)) {
|
||||
log(
|
||||
this.debug,
|
||||
LogType.Info,
|
||||
`ℹ ${relay.url} : Received Event`,
|
||||
e
|
||||
)
|
||||
eventIds.add(e.id) // Record the event ID
|
||||
events.push(e) // Add the event to the array
|
||||
}
|
||||
},
|
||||
// Handle the End-Of-Stream (EOSE) message
|
||||
oneose: () => {
|
||||
@ -187,6 +211,24 @@ export class RelayController {
|
||||
// Wait for all subscriptions to complete
|
||||
await Promise.allSettled(subPromises)
|
||||
|
||||
// Return the most recent event, or null if no events were received
|
||||
return events
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
||||
* If no relays are specified, it defaults to using connected relays.
|
||||
*
|
||||
* @param {Filter} filter - The filter criteria to find the event.
|
||||
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
||||
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
||||
*/
|
||||
fetchEvent = async (
|
||||
filter: Filter,
|
||||
relays: string[] = []
|
||||
): Promise<Event | null> => {
|
||||
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)
|
||||
|
||||
|
@ -28,11 +28,12 @@ export const Header = () => {
|
||||
} else {
|
||||
dispatch(setIsAuth(true))
|
||||
dispatch(setUser({ npub }))
|
||||
const metadataController = MetadataController.getInstance()
|
||||
metadataController.findMetadata(npub).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(setUser(userProfile))
|
||||
}
|
||||
MetadataController.getInstance().then((metadataController) => {
|
||||
metadataController.findMetadata(npub).then((userProfile) => {
|
||||
if (userProfile) {
|
||||
dispatch(setUser(userProfile))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -150,9 +150,21 @@ export const HomePage = () => {
|
||||
<h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
|
||||
</div>
|
||||
<div className='IBMSMList IBMSMListAlt'>
|
||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg'
|
||||
/>
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg'
|
||||
/>
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true'
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMAction'>
|
||||
<a
|
||||
@ -169,10 +181,26 @@ export const HomePage = () => {
|
||||
<h2 className='IBMSMTitleMainHeading'>Latest Mods</h2>
|
||||
</div>
|
||||
<div className='IBMSMList'>
|
||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg'
|
||||
/>
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg'
|
||||
/>
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true'
|
||||
/>
|
||||
<ModCard
|
||||
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='IBMSMAction'>
|
||||
|
@ -3,180 +3,114 @@ import '../styles/pagination.css'
|
||||
import '../styles/styles.css'
|
||||
import '../styles/search.css'
|
||||
import { ModCard } from '../components/ModCard'
|
||||
import { useDidMount } from '../hooks'
|
||||
import { Filter, kinds } from 'nostr-tools'
|
||||
import { RelayController } from '../controllers'
|
||||
import { constructModListFromEvents, log, LogType } from '../utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { useState } from 'react'
|
||||
import { ModDetails } from '../types'
|
||||
|
||||
enum SortByFilter {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
enum NSFWFilter {
|
||||
Hide_NSFW = 'Hide NSFW',
|
||||
Show_NSFW = 'Show NSFW',
|
||||
Only_NSFW = 'Only NSFW'
|
||||
}
|
||||
|
||||
enum ModeratedFilter {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated'
|
||||
}
|
||||
|
||||
export const ModsPage = () => {
|
||||
const [isFetching, setIsFetching] = useState(true)
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
|
||||
useDidMount(async () => {
|
||||
const filter: Filter = {
|
||||
kinds: [kinds.ClassifiedListing]
|
||||
}
|
||||
|
||||
RelayController.getInstance()
|
||||
.fetchEvents(filter, [])
|
||||
.then((events) => {
|
||||
const modList = constructModListFromEvents(events)
|
||||
setMods(modList)
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'An error occurred in fetching mods from relays',
|
||||
err
|
||||
)
|
||||
toast.error('An error occurred in fetching mods from relays')
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
})
|
||||
|
||||
if (isFetching)
|
||||
return <LoadingSpinner desc='Fetching mod details from relays' />
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSecMain'>
|
||||
<div className='SearchMainWrapper'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>Mods</h2>
|
||||
</div>
|
||||
<div className='SearchMain'>
|
||||
<div className='SearchMainInside'>
|
||||
<div className='SearchMainInsideWrapper'>
|
||||
<input type='text' className='SMIWInput' />
|
||||
<button className='btn btnMain SMIWButton' type='button'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Latest
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Latest
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Oldest
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Best Rated
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Worst Rated
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Show all (filtered)
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show all (filtered)
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show all (unfiltered)
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show from my follow list
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Hide NSFW
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Hide NSFW
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show NSFW
|
||||
<br />
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Only show NSFW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Show From: DEG Mods
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show From: DEG Mods
|
||||
</a>
|
||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||
Show All
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PageTitleRow />
|
||||
<Filters />
|
||||
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMList'>
|
||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
||||
{mods.map((mod, index) => (
|
||||
<ModCard
|
||||
key={`mod-${index}`}
|
||||
title={mod.title}
|
||||
summary={mod.summary}
|
||||
backgroundLink={mod.featuredImageUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='IBMSecMain'>
|
||||
<div className='PaginationMain'>
|
||||
<div className='PaginationMainInside'>
|
||||
<a
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
href='#'
|
||||
<Pagination />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const PageTitleRow = () => {
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='SearchMainWrapper'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>Mods</h2>
|
||||
</div>
|
||||
<div className='SearchMain'>
|
||||
<div className='SearchMainInside'>
|
||||
<div className='SearchMainInsideWrapper'>
|
||||
<input type='text' className='SMIWInput' />
|
||||
<button className='btn btnMain SMIWButton' type='button'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<i className='fas fa-chevron-left'></i>
|
||||
</a>
|
||||
<div className='PaginationMainInsideBoxGroup'>
|
||||
<a className='PaginationMainInsideBox PMIBActive' href='#'>
|
||||
<p>1</p>{' '}
|
||||
</a>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>2</p>{' '}
|
||||
</a>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>3</p>
|
||||
</a>
|
||||
<p className='PaginationMainInsideBox PMIBDots'>...</p>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>8</p>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
href='#'
|
||||
>
|
||||
<i className='fas fa-chevron-right'></i>
|
||||
</a>
|
||||
</div>
|
||||
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -184,3 +118,135 @@ export const ModsPage = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Filters = () => {
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Latest
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByFilter).map((item, index) => (
|
||||
<div
|
||||
key={`sortByFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Show all (filtered)
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilter).map((item, index) => (
|
||||
<div
|
||||
key={`moderatedFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Hide NSFW
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<div
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Show From: DEG Mods
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<div className='dropdown-item dropdownMainMenuItem'>
|
||||
Show From: {window.location.host}
|
||||
</div>
|
||||
<div className='dropdown-item dropdownMainMenuItem'>Show All</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Pagination = () => {
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='PaginationMain'>
|
||||
<div className='PaginationMainInside'>
|
||||
<a
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
href='#'
|
||||
>
|
||||
<i className='fas fa-chevron-left'></i>
|
||||
</a>
|
||||
<div className='PaginationMainInsideBoxGroup'>
|
||||
<a className='PaginationMainInsideBox PMIBActive' href='#'>
|
||||
<p>1</p>{' '}
|
||||
</a>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>2</p>{' '}
|
||||
</a>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>3</p>
|
||||
</a>
|
||||
<p className='PaginationMainInsideBox PMIBDots'>...</p>
|
||||
<a className='PaginationMainInsideBox' href='#'>
|
||||
<p>8</p>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
href='#'
|
||||
>
|
||||
<i className='fas fa-chevron-right'></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -42,3 +42,52 @@ export const extractModData = (event: Event): ModDetails => {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a list of `ModDetails` objects from an array of events.
|
||||
*
|
||||
* This function filters out events that do not contain all required data,
|
||||
* extracts the necessary information from the valid events, and constructs
|
||||
* `ModDetails` objects.
|
||||
*
|
||||
* @param events - The array of event objects to be processed.
|
||||
* @returns An array of `ModDetails` objects constructed from valid events.
|
||||
*/
|
||||
export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
|
||||
// Filter and extract mod details from events
|
||||
const modDetailsList: ModDetails[] = events
|
||||
.filter(isModDataComplete) // Filter out incomplete events
|
||||
.map((event) => extractModData(event)) // Extract data and construct ModDetails
|
||||
|
||||
return modDetailsList
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided event contains all the required data for constructing a `ModDetails` object.
|
||||
*
|
||||
* This function verifies that the event has the necessary tags and values to construct a `ModDetails` object.
|
||||
*
|
||||
* @param event - The event object to be checked.
|
||||
* @returns `true` if the event contains all required data; `false` otherwise.
|
||||
*/
|
||||
export const isModDataComplete = (event: Event): boolean => {
|
||||
// Helper function to check if a tag value is present and not empty
|
||||
const hasTagValue = (tagIdentifier: string): boolean => {
|
||||
const value = getTagValue(event, tagIdentifier)
|
||||
return !!value && value.length > 0 && value[0].trim() !== ''
|
||||
}
|
||||
|
||||
// Check if all required fields are present
|
||||
return (
|
||||
hasTagValue('t') &&
|
||||
hasTagValue('published_at') &&
|
||||
hasTagValue('game') &&
|
||||
hasTagValue('title') &&
|
||||
hasTagValue('featuredImageUrl') &&
|
||||
hasTagValue('summary') &&
|
||||
hasTagValue('nsfw') &&
|
||||
getTagValue(event, 'screenshotsUrls') !== null &&
|
||||
getTagValue(event, 'tags') !== null &&
|
||||
getTagValue(event, 'downloadUrls') !== null
|
||||
)
|
||||
}
|
||||
|
@ -52,3 +52,35 @@ export const getTagValue = (
|
||||
// Return null if no matching tag is found.
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hexKey hex private or public key
|
||||
* @returns whether or not is key valid
|
||||
*/
|
||||
const validateHex = (hexKey: string) => {
|
||||
return hexKey.match(/^[a-f0-9]{64}$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* NPUB provided - it will convert NPUB to HEX
|
||||
* HEX provided - it will return HEX
|
||||
*
|
||||
* @param pubKey in NPUB, HEX format
|
||||
* @returns HEX format
|
||||
*/
|
||||
export const npubToHex = (pubKey: string): string | null => {
|
||||
// If key is NPUB
|
||||
if (pubKey.startsWith('npub1')) {
|
||||
try {
|
||||
return nip19.decode(pubKey).data as string
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// valid hex key
|
||||
if (validateHex(pubKey)) return pubKey
|
||||
|
||||
// Not a valid hex key
|
||||
return null
|
||||
}
|
||||
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_RELAY: string
|
||||
readonly VITE_ADMIN_NPUBS: string
|
||||
// more env variables...
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user