feat: implemented mod details page #3

Merged
s merged 6 commits from mods-inner into master 2024-08-06 10:57:28 +00:00
10 changed files with 443 additions and 200 deletions
Showing only changes of commit 0c266dde6a - Show all commits

View File

@ -1 +1,4 @@
# 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 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>

View File

@ -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'>

View File

@ -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
} }

View File

@ -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)

View File

@ -28,12 +28,13 @@ 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))
} }
}) })
})
} }
} }
}) })

View File

@ -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&amp;imh=256&amp;ima=fit&amp;impolicy=Letterbox&amp;imcolor=%23000000&amp;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&amp;imh=256&amp;ima=fit&amp;impolicy=Letterbox&amp;imcolor=%23000000&amp;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&amp;imh=256&amp;ima=fit&amp;impolicy=Letterbox&amp;imcolor=%23000000&amp;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&amp;imh=256&amp;ima=fit&amp;impolicy=Letterbox&amp;imcolor=%23000000&amp;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'>

View File

@ -3,12 +3,94 @@ 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'>
<PageTitleRow />
<Filters />
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'>
{mods.map((mod, index) => (
<ModCard
key={`mod-${index}`}
title={mod.title}
summary={mod.summary}
backgroundLink={mod.featuredImageUrl}
/>
))}
</div>
</div>
<Pagination />
</div>
</div>
</div>
)
}
const PageTitleRow = () => {
return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='SearchMainWrapper'> <div className='SearchMainWrapper'>
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
@ -34,7 +116,11 @@ export const ModsPage = () => {
</div> </div>
</div> </div>
</div> </div>
)
}
const Filters = () => {
return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='FiltersMain'> <div className='FiltersMain'>
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
@ -47,19 +133,16 @@ export const ModsPage = () => {
> >
Latest Latest
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
<a className='dropdown-item dropdownMainMenuItem' href='#'> {Object.values(SortByFilter).map((item, index) => (
Latest <div
</a> key={`sortByFilterItem-${index}`}
<a className='dropdown-item dropdownMainMenuItem' href='#'> className='dropdown-item dropdownMainMenuItem'
Oldest >
</a> {item}
<a className='dropdown-item dropdownMainMenuItem' href='#'> </div>
Best Rated ))}
</a>
<a className='dropdown-item dropdownMainMenuItem' href='#'>
Worst Rated
</a>
</div> </div>
</div> </div>
</div> </div>
@ -74,15 +157,14 @@ export const ModsPage = () => {
Show all (filtered) Show all (filtered)
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
<a className='dropdown-item dropdownMainMenuItem' href='#'> {Object.values(ModeratedFilter).map((item, index) => (
Show all (filtered) <div
</a> key={`moderatedFilterItem-${index}`}
<a className='dropdown-item dropdownMainMenuItem' href='#'> className='dropdown-item dropdownMainMenuItem'
Show all (unfiltered) >
</a> {item}
<a className='dropdown-item dropdownMainMenuItem' href='#'> </div>
Show from my follow list ))}
</a>
</div> </div>
</div> </div>
</div> </div>
@ -97,16 +179,14 @@ export const ModsPage = () => {
Hide NSFW Hide NSFW
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
<a className='dropdown-item dropdownMainMenuItem' href='#'> {Object.values(NSFWFilter).map((item, index) => (
Hide NSFW <div
</a> key={`nsfwFilterItem-${index}`}
<a className='dropdown-item dropdownMainMenuItem' href='#'> className='dropdown-item dropdownMainMenuItem'
Show NSFW >
<br /> {item}
</a> </div>
<a className='dropdown-item dropdownMainMenuItem' href='#'> ))}
Only show NSFW
</a>
</div> </div>
</div> </div>
</div> </div>
@ -121,31 +201,20 @@ export const ModsPage = () => {
Show From: DEG Mods Show From: DEG Mods
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
<a className='dropdown-item dropdownMainMenuItem' href='#'> <div className='dropdown-item dropdownMainMenuItem'>
Show From: DEG Mods Show From: {window.location.host}
</a> </div>
<a className='dropdown-item dropdownMainMenuItem' href='#'> <div className='dropdown-item dropdownMainMenuItem'>Show All</div>
Show All
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)
}
<div className='IBMSecMain IBMSMListWrapper'> const Pagination = () => {
<div className='IBMSMList'> return (
<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&amp;imh=256&amp;ima=fit&amp;impolicy=Letterbox&amp;imcolor=%23000000&amp;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' />
</div>
</div>
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='PaginationMain'> <div className='PaginationMain'>
<div className='PaginationMainInside'> <div className='PaginationMainInside'>
@ -179,8 +248,5 @@ export const ModsPage = () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
) )
} }

View File

@ -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
)
}

View File

@ -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
View File

@ -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...
} }