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'
|
import '../styles/cardMod.css'
|
||||||
|
|
||||||
type ModCardProps = {
|
type ModCardProps = {
|
||||||
|
title: string
|
||||||
|
summary: string
|
||||||
backgroundLink: string
|
backgroundLink: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModCard = ({ backgroundLink }: ModCardProps) => {
|
export const ModCard = ({ title, summary, backgroundLink }: ModCardProps) => {
|
||||||
return (
|
return (
|
||||||
<a className='cardModMainWrapperLink' href='mods-inner.html'>
|
<a className='cardModMainWrapperLink' href='mods-inner.html'>
|
||||||
<div className='cardModMain'>
|
<div className='cardModMain'>
|
||||||
@ -15,21 +17,8 @@ export const ModCard = ({ backgroundLink }: ModCardProps) => {
|
|||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<div className='cMMBody'>
|
<div className='cMMBody'>
|
||||||
<h3 className='cMMBodyTitle'>
|
<h3 className='cMMBodyTitle'>{title}</h3>
|
||||||
This is a mod title for an awesome game that will make everyone
|
<p className='cMMBodyText'>{summary}</p>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='cMMFoot'>
|
<div className='cMMFoot'>
|
||||||
<div className='cMMFootReactions'>
|
<div className='cMMFootReactions'>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import NDK, { NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
|
import NDK, { NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
|
||||||
import { UserProfile } from '../types/user'
|
import { UserProfile } from '../types/user'
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class to manage metadata operations using NDK.
|
* Singleton class to manage metadata operations using NDK.
|
||||||
@ -8,12 +8,42 @@ import { hexToNpub } from '../utils'
|
|||||||
export class MetadataController {
|
export class MetadataController {
|
||||||
private static instance: MetadataController
|
private static instance: MetadataController
|
||||||
private profileNdk: NDK
|
private profileNdk: NDK
|
||||||
|
public adminNpubs: string[]
|
||||||
|
public adminRelays = new Set<string>()
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.profileNdk = new NDK({
|
this.profileNdk = new NDK({
|
||||||
explicitRelayUrls: ['wss://user.kindpag.es', 'wss://purplepag.es']
|
explicitRelayUrls: ['wss://user.kindpag.es', 'wss://purplepag.es']
|
||||||
})
|
})
|
||||||
this.profileNdk.connect()
|
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.
|
* @returns The singleton instance of MetadataController.
|
||||||
*/
|
*/
|
||||||
public static getInstance(): MetadataController {
|
public static async getInstance(): Promise<MetadataController> {
|
||||||
if (!MetadataController.instance) {
|
if (!MetadataController.instance) {
|
||||||
MetadataController.instance = new MetadataController()
|
MetadataController.instance = new MetadataController()
|
||||||
|
|
||||||
|
MetadataController.instance.setAdminRelays()
|
||||||
}
|
}
|
||||||
return MetadataController.instance
|
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
|
// 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
|
// 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
|
// Use a timeout to handle cases where retrieving write relays takes too long
|
||||||
const writeRelaysPromise = MetadataController.getInstance().findWriteRelays(
|
const writeRelaysPromise = metadataController.findWriteRelays(event.pubkey)
|
||||||
event.pubkey
|
|
||||||
)
|
|
||||||
|
|
||||||
log(this.debug, LogType.Info, `ℹ Finding user's write relays`)
|
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
|
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
|
// Connect to all write relays obtained from MetadataController
|
||||||
const relayPromises = writeRelayUrls.map((relayUrl) =>
|
const relayPromises = writeRelayUrls.map((relayUrl) =>
|
||||||
this.connectRelay(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.
|
* If no relays are specified, it defaults to using connected relays.
|
||||||
*
|
*
|
||||||
* @param {Filter} filter - The filter criteria to find the event.
|
* @param {Filter} filter - The filter criteria to find the event.
|
||||||
* @param {string[]} [relays] - An optional array of relay URLs to search for 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.
|
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
||||||
*/
|
*/
|
||||||
fetchEvent = async (
|
fetchEvents = async (
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
relays: string[] = []
|
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
|
// Connect to all specified relays
|
||||||
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
|
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
|
||||||
await Promise.allSettled(relayPromises)
|
await Promise.allSettled(relayPromises)
|
||||||
@ -163,6 +177,7 @@ export class RelayController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const events: Event[] = []
|
const events: Event[] = []
|
||||||
|
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
||||||
|
|
||||||
// Create a promise for each relay subscription
|
// Create a promise for each relay subscription
|
||||||
const subPromises = this.connectedRelays.map((relay) => {
|
const subPromises = this.connectedRelays.map((relay) => {
|
||||||
@ -171,8 +186,17 @@ export class RelayController {
|
|||||||
const sub = relay.subscribe([filter], {
|
const sub = relay.subscribe([filter], {
|
||||||
// Handle incoming events
|
// Handle incoming events
|
||||||
onevent: (e) => {
|
onevent: (e) => {
|
||||||
log(this.debug, LogType.Info, `ℹ ${relay.url} : Received Event`, e)
|
// Add the event to the array if it's not a duplicate
|
||||||
events.push(e)
|
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
|
// Handle the End-Of-Stream (EOSE) message
|
||||||
oneose: () => {
|
oneose: () => {
|
||||||
@ -187,6 +211,24 @@ export class RelayController {
|
|||||||
// Wait for all subscriptions to complete
|
// Wait for all subscriptions to complete
|
||||||
await Promise.allSettled(subPromises)
|
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
|
// 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)
|
||||||
|
|
||||||
|
@ -28,11 +28,12 @@ export const Header = () => {
|
|||||||
} else {
|
} else {
|
||||||
dispatch(setIsAuth(true))
|
dispatch(setIsAuth(true))
|
||||||
dispatch(setUser({ npub }))
|
dispatch(setUser({ npub }))
|
||||||
const metadataController = MetadataController.getInstance()
|
MetadataController.getInstance().then((metadataController) => {
|
||||||
metadataController.findMetadata(npub).then((userProfile) => {
|
metadataController.findMetadata(npub).then((userProfile) => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
dispatch(setUser(userProfile))
|
dispatch(setUser(userProfile))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,9 +150,21 @@ export const HomePage = () => {
|
|||||||
<h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
|
<h2 className='IBMSMTitleMainHeading'>Awesome Mods</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMList IBMSMListAlt'>
|
<div className='IBMSMList IBMSMListAlt'>
|
||||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
<ModCard
|
||||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
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>
|
||||||
<div className='IBMSMAction'>
|
<div className='IBMSMAction'>
|
||||||
<a
|
<a
|
||||||
@ -169,10 +181,26 @@ export const HomePage = () => {
|
|||||||
<h2 className='IBMSMTitleMainHeading'>Latest Mods</h2>
|
<h2 className='IBMSMTitleMainHeading'>Latest Mods</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMList'>
|
<div className='IBMSMList'>
|
||||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
<ModCard
|
||||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
title='This is a mod title for an awesome game that will make everyone happy! The happiest!'
|
||||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
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>
|
||||||
|
|
||||||
<div className='IBMSMAction'>
|
<div className='IBMSMAction'>
|
||||||
|
@ -3,180 +3,114 @@ import '../styles/pagination.css'
|
|||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import '../styles/search.css'
|
import '../styles/search.css'
|
||||||
import { ModCard } from '../components/ModCard'
|
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 = () => {
|
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 (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||||
<div className='IBMSecMain'>
|
<PageTitleRow />
|
||||||
<div className='SearchMainWrapper'>
|
<Filters />
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMList'>
|
<div className='IBMSMList'>
|
||||||
<ModCard backgroundLink='https://image.nostr.build/65a11a00bb99c11561735f861c51b498cf9dc07d02beff7303fe7f7ab52f3987.jpg' />
|
{mods.map((mod, index) => (
|
||||||
<ModCard backgroundLink='https://web.archive.org/web/20240215093752im_/https://staticdelivery.nexusmods.com/mods/6144/images/headers/13_1707966408.jpg' />
|
<ModCard
|
||||||
<ModCard backgroundLink='https://steamuserimages-a.akamaihd.net/ugc/2013708095892656347/39A93A2B1EB05E725214373849F8E37FCEB4C2EA/?imw=512&imh=256&ima=fit&impolicy=Letterbox&imcolor=%23000000&letterbox=true' />
|
key={`mod-${index}`}
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
title={mod.title}
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
summary={mod.summary}
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
backgroundLink={mod.featuredImageUrl}
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
/>
|
||||||
<ModCard backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png' />
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='IBMSecMain'>
|
<Pagination />
|
||||||
<div className='PaginationMain'>
|
</div>
|
||||||
<div className='PaginationMainInside'>
|
</div>
|
||||||
<a
|
</div>
|
||||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
)
|
||||||
href='#'
|
}
|
||||||
|
|
||||||
|
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>
|
<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>
|
||||||
</a>
|
</svg>
|
||||||
<div className='PaginationMainInsideBoxGroup'>
|
</button>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -184,3 +118,135 @@ export const ModsPage = () => {
|
|||||||
</div>
|
</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 if no matching tag is found.
|
||||||
return null
|
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 {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_RELAY: string
|
readonly VITE_APP_RELAY: string
|
||||||
|
readonly VITE_ADMIN_NPUBS: string
|
||||||
// more env variables...
|
// more env variables...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user