Compare commits
23 Commits
4233ad4ce7
...
9cb3d2fb63
Author | SHA1 | Date | |
---|---|---|---|
|
9cb3d2fb63 | ||
|
b9d5bb8211 | ||
|
4dc65b92f7 | ||
|
9e8aa16297 | ||
|
a90e932ed6 | ||
|
9730fec14f | ||
|
06f0282cad | ||
|
5b641ff4cc | ||
|
49ed027b5c | ||
|
aa8d18ea53 | ||
|
f708dd6530 | ||
|
e3f49832f2 | ||
|
d76676c089 | ||
|
6b1d4e7322 | ||
|
05adb00072 | ||
|
4175ebc010 | ||
|
3a71a4a297 | ||
|
d3a93eab3e | ||
|
22fc2b4ba3 | ||
|
d70e302a69 | ||
|
8b93d0506d | ||
|
a56d26387e | ||
|
d13c7ca6c3 |
@ -15,10 +15,6 @@ export const BlogCard = ({ backgroundLink }: BlogCardProps) => {
|
||||
>
|
||||
<div
|
||||
className='cardBlogMainInside'
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient( rgba(255, 255, 255, 0) 0%, #232323 100%)'
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
|
48
src/components/ErrorBoundary.tsx
Normal file
48
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { Component, ErrorInfo, ReactNode } from 'react'
|
||||
|
||||
// Define the state interface for error boundary
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
// Define the props interface (if you want to pass any props)
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
// Update state so the next render will show the fallback UI.
|
||||
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
// Log the error and error info (optional)
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
console.error('Uncaught error:', error, errorInfo)
|
||||
// You could also send the error to a logging service here
|
||||
console.error('props', this.props)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// You can render any fallback UI here
|
||||
return (
|
||||
<div>
|
||||
<h1>Oops! Something went wrong.</h1>
|
||||
<p>Please check console.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// If no error, render children
|
||||
return this.props.children
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import '../styles/cardMod.css'
|
||||
import { handleModImageError } from '../utils'
|
||||
|
||||
@ -6,27 +8,13 @@ type ModCardProps = {
|
||||
gameName: string
|
||||
summary: string
|
||||
imageUrl: string
|
||||
link: string
|
||||
handleClick: () => void
|
||||
route: string
|
||||
}
|
||||
|
||||
export const ModCard = ({
|
||||
title,
|
||||
gameName,
|
||||
summary,
|
||||
imageUrl,
|
||||
link,
|
||||
handleClick
|
||||
}: ModCardProps) => {
|
||||
export const ModCard = React.memo(
|
||||
({ title, gameName, summary, imageUrl, route }: ModCardProps) => {
|
||||
return (
|
||||
<a
|
||||
className='cardModMainWrapperLink'
|
||||
href={link}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
handleClick()
|
||||
}}
|
||||
>
|
||||
<Link className='cardModMainWrapperLink' to={route}>
|
||||
<div className='cardModMain'>
|
||||
<div className='cMMPictureWrapper'>
|
||||
<img
|
||||
@ -95,6 +83,7 @@ export const ModCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
40
src/components/Pagination.tsx
Normal file
40
src/components/Pagination.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
|
||||
type PaginationProps = {
|
||||
page: number
|
||||
disabledNext: boolean
|
||||
handlePrev: () => void
|
||||
handleNext: () => void
|
||||
}
|
||||
|
||||
export const Pagination = React.memo(
|
||||
({ page, disabledNext, handlePrev, handleNext }: PaginationProps) => {
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='PaginationMain'>
|
||||
<div className='PaginationMainInside'>
|
||||
<button
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
onClick={handlePrev}
|
||||
disabled={page === 1}
|
||||
>
|
||||
<i className='fas fa-chevron-left'></i>
|
||||
</button>
|
||||
<div className='PaginationMainInsideBoxGroup'>
|
||||
<button className='PaginationMainInsideBox PMIBActive'>
|
||||
<p>{page}</p>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
onClick={handleNext}
|
||||
disabled={disabledNext}
|
||||
>
|
||||
<i className='fas fa-chevron-right'></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import {
|
||||
MetadataController,
|
||||
@ -14,16 +14,17 @@ import '../styles/author.css'
|
||||
import '../styles/innerPage.css'
|
||||
import '../styles/socialPosts.css'
|
||||
import { UserProfile } from '../types'
|
||||
import { copyTextToClipboard, log, LogType, now } from '../utils'
|
||||
import { copyTextToClipboard, log, LogType, now, npubToHex } from '../utils'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
import { ZapPopUp } from './Zap'
|
||||
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
|
||||
import _ from 'lodash'
|
||||
|
||||
type Props = {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export const ProfileSection = ({ pubkey }: Props) => {
|
||||
const navigate = useNavigate()
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(async () => {
|
||||
@ -33,110 +34,13 @@ export const ProfileSection = ({ pubkey }: Props) => {
|
||||
})
|
||||
})
|
||||
|
||||
const handleCopy = async () => {
|
||||
copyTextToClipboard(profile?.npub as string).then((isCopied) => {
|
||||
if (isCopied) {
|
||||
toast.success('Npub copied to clipboard!')
|
||||
} else {
|
||||
toast.error(
|
||||
'Failed to copy, look into console for more details on error!'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!profile) return null
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='IBMSMSplitMainSmallSide'>
|
||||
<div className='IBMSMSplitMainSmallSideSecWrapper'>
|
||||
<div className='IBMSMSplitMainSmallSideSec'>
|
||||
<div className='IBMSMSMSSS_Author'>
|
||||
<div className='IBMSMSMSSS_Author_Top'>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left'>
|
||||
<a
|
||||
className='IBMSMSMSSS_Author_Top_Left_InsideLinkWrapper'
|
||||
href={`#${profileRoute}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
navigate(profileRoute)
|
||||
}}
|
||||
>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_Inside'>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_InsidePic'>
|
||||
<div className='IBMSMSMSSS_Author_Top_PPWrapper'>
|
||||
<div
|
||||
className='IBMSMSMSSS_Author_Top_PP'
|
||||
style={{
|
||||
background: `url('${
|
||||
profile.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
|
||||
<div className='IBMSMSMSSS_Author_TopWrapper'>
|
||||
<p className='IBMSMSMSSS_Author_Top_Name'>
|
||||
{profile.displayName || profile.name || ''}
|
||||
</p>
|
||||
<p className='IBMSMSMSSS_Author_Top_Handle'>
|
||||
{profile.nip05 || ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div className='IBMSMSMSSS_Author_Top_AddressWrapper'>
|
||||
<div className='IBMSMSMSSS_Author_Top_AddressWrapped'>
|
||||
<p
|
||||
id='SiteOwnerAddress'
|
||||
className='IBMSMSMSSS_Author_Top_Address'
|
||||
>
|
||||
{profile.npub}
|
||||
</p>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_IconWrapper'>
|
||||
<div
|
||||
id='copySiteOwnerAddress'
|
||||
className='IBMSMSMSSS_Author_Top_IconWrapped'
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<QRButtonWithPopUp pubkey={profile.pubkey as string} />
|
||||
<ZapButtonWithPopUp pubkey={profile.pubkey as string} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_Details'>
|
||||
<p className='IBMSMSMSSS_Author_Top_Bio'>
|
||||
{profile.bio || profile.about}
|
||||
</p>
|
||||
<div
|
||||
id='OwnerFollowLogin'
|
||||
className='IBMSMSMSSS_Author_Top_NostrLinks'
|
||||
style={{ display: 'flex' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<FollowButton pubkey={pubkey} />
|
||||
</div>
|
||||
<Profile profile={profile} />
|
||||
</div>
|
||||
<div className='IBMSMSplitMainSmallSideSec'>
|
||||
<div className='IBMSMSMSSS_ShortPosts'>
|
||||
@ -186,6 +90,117 @@ export const ProfileSection = ({ pubkey }: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
type ProfileProps = {
|
||||
profile: NDKUserProfile
|
||||
}
|
||||
|
||||
export const Profile = ({ profile }: ProfileProps) => {
|
||||
const handleCopy = async () => {
|
||||
copyTextToClipboard(profile.npub as string).then((isCopied) => {
|
||||
if (isCopied) {
|
||||
toast.success('Npub copied to clipboard!')
|
||||
} else {
|
||||
toast.error(
|
||||
'Failed to copy, look into console for more details on error!'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const hexPubkey = npubToHex(profile.pubkey as string)
|
||||
|
||||
if (!hexPubkey) return null
|
||||
|
||||
const profileRoute = getProfilePageRoute(
|
||||
nip19.nprofileEncode({
|
||||
pubkey: hexPubkey
|
||||
})
|
||||
)
|
||||
|
||||
const npub = (profile.npub as string) || ''
|
||||
const displayName =
|
||||
profile.displayName ||
|
||||
profile.name ||
|
||||
_.truncate(npub, {
|
||||
length: 16
|
||||
})
|
||||
const nip05 = profile.nip05 || ''
|
||||
const about = profile.bio || profile.about || ''
|
||||
|
||||
return (
|
||||
<div className='IBMSMSMSSS_Author'>
|
||||
<div className='IBMSMSMSSS_Author_Top'>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left'>
|
||||
<Link
|
||||
className='IBMSMSMSSS_Author_Top_Left_InsideLinkWrapper'
|
||||
to={profileRoute}
|
||||
>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_Inside'>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_InsidePic'>
|
||||
<div className='IBMSMSMSSS_Author_Top_PPWrapper'>
|
||||
<div
|
||||
className='IBMSMSMSSS_Author_Top_PP'
|
||||
style={{
|
||||
background: `url('${
|
||||
profile.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
|
||||
<div className='IBMSMSMSSS_Author_TopWrapper'>
|
||||
<p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p>
|
||||
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<div className='IBMSMSMSSS_Author_Top_AddressWrapper'>
|
||||
<div className='IBMSMSMSSS_Author_Top_AddressWrapped'>
|
||||
<p
|
||||
id='SiteOwnerAddress'
|
||||
className='IBMSMSMSSS_Author_Top_Address'
|
||||
>
|
||||
{npub}
|
||||
</p>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_IconWrapper'>
|
||||
<div
|
||||
id='copySiteOwnerAddress'
|
||||
className='IBMSMSMSSS_Author_Top_IconWrapped'
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='IBMSMSMSSS_Author_Top_Icon'
|
||||
>
|
||||
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
<QRButtonWithPopUp pubkey={hexPubkey} />
|
||||
<ZapButtonWithPopUp pubkey={hexPubkey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='IBMSMSMSSS_Author_Top_Details'>
|
||||
<p className='IBMSMSMSSS_Author_Top_Bio'>{about}</p>
|
||||
<div
|
||||
id='OwnerFollowLogin'
|
||||
className='IBMSMSMSSS_Author_Top_NostrLinks'
|
||||
style={{ display: 'flex' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<FollowButton pubkey={hexPubkey} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface Post {
|
||||
name: string
|
||||
link: string
|
||||
|
@ -368,24 +368,46 @@ export class RelayController {
|
||||
*/
|
||||
fetchEvents = async (
|
||||
filter: Filter,
|
||||
relays: string[] = []
|
||||
relayUrls: string[] = []
|
||||
): Promise<Event[]> => {
|
||||
// add app relay to relays array
|
||||
relays.push(import.meta.env.VITE_APP_RELAY)
|
||||
const relaySet = new Set<string>()
|
||||
|
||||
// add all the relays passed to relay set
|
||||
relayUrls.forEach((relayUrl) => {
|
||||
relaySet.add(relayUrl)
|
||||
})
|
||||
|
||||
relaySet.add(import.meta.env.VITE_APP_RELAY)
|
||||
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
// add admin relays to relays array
|
||||
metadataController.adminRelays.forEach((url) => {
|
||||
relays.push(url)
|
||||
metadataController.adminRelays.forEach((relayUrl) => {
|
||||
relaySet.add(relayUrl)
|
||||
})
|
||||
|
||||
relayUrls = Array.from(relaySet)
|
||||
|
||||
// Connect to all specified relays
|
||||
const relayPromises = relays.map((relayUrl) => this.connectRelay(relayUrl))
|
||||
await Promise.allSettled(relayPromises)
|
||||
const relayPromises = relayUrls.map((relayUrl) =>
|
||||
this.connectRelay(relayUrl)
|
||||
)
|
||||
|
||||
// Use Promise.allSettled to wait for all promises to settle
|
||||
const results = await Promise.allSettled(relayPromises)
|
||||
|
||||
// Extract non-null values from fulfilled promises in a single pass
|
||||
const relays = results.reduce<Relay[]>((acc, result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const value = result.value
|
||||
if (value) {
|
||||
acc.push(value)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Check if any relays are connected
|
||||
if (this.connectedRelays.length === 0) {
|
||||
log(this.debug, LogType.Error, 'No relay is connected to fetch events!')
|
||||
if (relays.length === 0) {
|
||||
throw new Error('No relay is connected to fetch events!')
|
||||
}
|
||||
|
||||
@ -393,7 +415,7 @@ export class RelayController {
|
||||
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) => {
|
||||
const subPromises = relays.map((relay) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
// Subscribe to the relay with the specified filter
|
||||
const sub = relay.subscribe([filter], {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Footer } from './footer'
|
||||
import { Header } from './header'
|
||||
import { SocialNav } from './socialNav'
|
||||
|
||||
export const Layout = () => {
|
||||
return (
|
||||
@ -8,6 +9,7 @@ export const Layout = () => {
|
||||
<Header />
|
||||
<Outlet />
|
||||
<Footer />
|
||||
<SocialNav />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
115
src/layout/socialNav.tsx
Normal file
115
src/layout/socialNav.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { appRoutes, getProfilePageRoute } from 'routes'
|
||||
import 'styles/socialNav.css'
|
||||
|
||||
export const SocialNav = () => {
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
|
||||
|
||||
const toggleNav = () => {
|
||||
setIsCollapsed(!isCollapsed)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='socialNav'
|
||||
style={{
|
||||
transform: isCollapsed ? 'translateX(0)' : 'translateX(50%)',
|
||||
right: isCollapsed ? '0%' : '50%'
|
||||
}}
|
||||
>
|
||||
<div className='socialNavInsideWrapper'>
|
||||
{!isCollapsed && (
|
||||
<div className='socialNavInside'>
|
||||
<Link
|
||||
to={appRoutes.home}
|
||||
className='btn btnMain socialNavInsideBtn socialNavInsideBtnActive'
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 -32 576 576'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M511.8 287.6L512.5 447.7C512.5 450.5 512.3 453.1 512 455.8V472C512 494.1 494.1 512 472 512H456C454.9 512 453.8 511.1 452.7 511.9C451.3 511.1 449.9 512 448.5 512H392C369.9 512 352 494.1 352 472V384C352 366.3 337.7 352 320 352H256C238.3 352 224 366.3 224 384V472C224 494.1 206.1 512 184 512H128.1C126.6 512 125.1 511.9 123.6 511.8C122.4 511.9 121.2 512 120 512H104C81.91 512 64 494.1 64 472V360C64 359.1 64.03 358.1 64.09 357.2V287.6H32.05C14.02 287.6 0 273.5 0 255.5C0 246.5 3.004 238.5 10.01 231.5L266.4 8.016C273.4 1.002 281.4 0 288.4 0C295.4 0 303.4 2.004 309.5 7.014L416 100.7V64C416 46.33 430.3 32 448 32H480C497.7 32 512 46.33 512 64V185L564.8 231.5C572.8 238.5 576.9 246.5 575.8 255.5C575.8 273.5 560.8 287.6 543.8 287.6L511.8 287.6z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
className='btn btnMain socialNavInsideBtn'
|
||||
to={appRoutes.home}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M88 48C101.3 48 112 58.75 112 72V120C112 133.3 101.3 144 88 144H40C26.75 144 16 133.3 16 120V72C16 58.75 26.75 48 40 48H88zM480 64C497.7 64 512 78.33 512 96C512 113.7 497.7 128 480 128H192C174.3 128 160 113.7 160 96C160 78.33 174.3 64 192 64H480zM480 224C497.7 224 512 238.3 512 256C512 273.7 497.7 288 480 288H192C174.3 288 160 273.7 160 256C160 238.3 174.3 224 192 224H480zM480 384C497.7 384 512 398.3 512 416C512 433.7 497.7 448 480 448H192C174.3 448 160 433.7 160 416C160 398.3 174.3 384 192 384H480zM16 232C16 218.7 26.75 208 40 208H88C101.3 208 112 218.7 112 232V280C112 293.3 101.3 304 88 304H40C26.75 304 16 293.3 16 280V232zM88 368C101.3 368 112 378.7 112 392V440C112 453.3 101.3 464 88 464H40C26.75 464 16 453.3 16 440V392C16 378.7 26.75 368 40 368H88z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
className='btn btnMain socialNavInsideBtn'
|
||||
to={appRoutes.home}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-32 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M256 32V51.2C329 66.03 384 130.6 384 208V226.8C384 273.9 401.3 319.2 432.5 354.4L439.9 362.7C448.3 372.2 450.4 385.6 445.2 397.1C440 408.6 428.6 416 416 416H32C19.4 416 7.971 408.6 2.809 397.1C-2.353 385.6-.2883 372.2 8.084 362.7L15.5 354.4C46.74 319.2 64 273.9 64 226.8V208C64 130.6 118.1 66.03 192 51.2V32C192 14.33 206.3 0 224 0C241.7 0 256 14.33 256 32H256zM224 512C207 512 190.7 505.3 178.7 493.3C166.7 481.3 160 464.1 160 448H288C288 464.1 281.3 481.3 269.3 493.3C257.3 505.3 240.1 512 224 512z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
className='btn btnMain socialNavInsideBtn'
|
||||
to={appRoutes.search}
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
<Link
|
||||
className='btn btnMain socialNavInsideBtn'
|
||||
to={getProfilePageRoute('xyz')}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
>
|
||||
<path d='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z'></path>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='socialNavCollapse' onClick={toggleNav}>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='-128 0 512 512'
|
||||
width='1em'
|
||||
height='1em'
|
||||
fill='currentColor'
|
||||
className='socialNavCollapseIcon'
|
||||
style={{
|
||||
transform: isCollapsed ? 'rotate(0deg)' : 'rotate(180deg)'
|
||||
}}
|
||||
>
|
||||
<path d='M192 448c-8.188 0-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25l160-160c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L77.25 256l137.4 137.4c12.5 12.5 12.5 32.75 0 45.25C208.4 444.9 200.2 448 192 448z'></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -181,10 +181,12 @@ const SlideContent = ({ naddr }: SlideContentProps) => {
|
||||
</div>
|
||||
<div className='IBMSMSCWSInfo'>
|
||||
<h3 className='IBMSMSCWSInfoHeading'>{mod.title}</h3>
|
||||
<div className='IBMSMSCWSInfoTextWrapper'>
|
||||
<p className='IBMSMSCWSInfoText'>
|
||||
{mod.summary}
|
||||
<br />
|
||||
</p>
|
||||
</div>
|
||||
<p className='IBMSMSCWSInfoText IBMSMSCWSInfoText2'>
|
||||
{mod.game}
|
||||
<br />
|
||||
@ -208,7 +210,6 @@ type DisplayModProps = {
|
||||
}
|
||||
|
||||
const DisplayMod = ({ naddr }: DisplayModProps) => {
|
||||
const navigate = useNavigate()
|
||||
const [mod, setMod] = useState<ModDetails>()
|
||||
|
||||
useDidMount(() => {
|
||||
@ -249,8 +250,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
|
||||
gameName={mod.game}
|
||||
summary={mod.summary}
|
||||
imageUrl={mod.featuredImageUrl}
|
||||
link={`#${route}`}
|
||||
handleClick={() => navigate(route)}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -298,8 +298,7 @@ const DisplayLatestMods = () => {
|
||||
gameName={mod.game}
|
||||
summary={mod.summary}
|
||||
imageUrl={mod.featuredImageUrl}
|
||||
link={`#${route}`}
|
||||
handleClick={() => navigate(route)}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
} from 'nostr-tools'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { Dispatch, SetStateAction, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getProfilePageRoute } from 'routes'
|
||||
import { ModDetails, UserProfile } from 'types'
|
||||
@ -355,8 +355,6 @@ const Filter = React.memo(
|
||||
)
|
||||
|
||||
const Comment = (props: CommentEvent) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [profile, setProfile] = useState<UserProfile>()
|
||||
|
||||
useDidMount(async () => {
|
||||
@ -376,19 +374,15 @@ const Comment = (props: CommentEvent) => {
|
||||
<div className='IBMSMSMBSSCL_Comment'>
|
||||
<div className='IBMSMSMBSSCL_CommentTop'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopPPWrapper'>
|
||||
<a
|
||||
<Link
|
||||
className='IBMSMSMBSSCL_CommentTopPP'
|
||||
href={`#${profileRoute}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
navigate(profileRoute)
|
||||
}}
|
||||
to={profileRoute}
|
||||
style={{
|
||||
background: `url('${
|
||||
profile?.image || ''
|
||||
}') center / cover no-repeat`
|
||||
}}
|
||||
></a>
|
||||
/>
|
||||
</div>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
|
||||
<div className='IBMSMSMBSSCL_CommentTopDetails'>
|
||||
|
@ -7,7 +7,6 @@ import React, {
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { ModCard } from '../components/ModCard'
|
||||
import { MetadataController } from '../controllers'
|
||||
@ -20,6 +19,7 @@ import '../styles/styles.css'
|
||||
import { ModDetails, MuteLists } from '../types'
|
||||
import { fetchMods } from '../utils'
|
||||
import { MOD_FILTER_LIMIT } from '../constants'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
|
||||
enum SortBy {
|
||||
Latest = 'Latest',
|
||||
@ -48,7 +48,6 @@ interface FilterOptions {
|
||||
}
|
||||
|
||||
export const ModsPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
@ -223,8 +222,7 @@ export const ModsPage = () => {
|
||||
gameName={mod.game}
|
||||
summary={mod.summary}
|
||||
imageUrl={mod.featuredImageUrl}
|
||||
link={`#${route}`}
|
||||
handleClick={() => navigate(route)}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -424,42 +422,3 @@ const Filters = React.memo(
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
type PaginationProps = {
|
||||
page: number
|
||||
disabledNext: boolean
|
||||
handlePrev: () => void
|
||||
handleNext: () => void
|
||||
}
|
||||
|
||||
const Pagination = React.memo(
|
||||
({ page, disabledNext, handlePrev, handleNext }: PaginationProps) => {
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='PaginationMain'>
|
||||
<div className='PaginationMainInside'>
|
||||
<button
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
onClick={handlePrev}
|
||||
disabled={page === 1}
|
||||
>
|
||||
<i className='fas fa-chevron-left'></i>
|
||||
</button>
|
||||
<div className='PaginationMainInsideBoxGroup'>
|
||||
<button className='PaginationMainInsideBox PMIBActive'>
|
||||
<p>{page}</p>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
||||
onClick={handleNext}
|
||||
disabled={disabledNext}
|
||||
>
|
||||
<i className='fas fa-chevron-right'></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
652
src/pages/search.tsx
Normal file
652
src/pages/search.tsx
Normal file
@ -0,0 +1,652 @@
|
||||
import { NDKEvent, NDKUserProfile, profileFromEvent } from '@nostr-dev-kit/ndk'
|
||||
import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||
import { GameCard } from 'components/GameCard'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ModCard } from 'components/ModCard'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
import { Profile } from 'components/ProfileSection'
|
||||
import { T_TAG_VALUE } from 'constants.ts'
|
||||
import { MetadataController, RelayController } from 'controllers'
|
||||
import { useAppSelector, useDidMount } from 'hooks'
|
||||
import { Filter, kinds, nip19 } from 'nostr-tools'
|
||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
||||
import Papa from 'papaparse'
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import { getModPageRoute } from 'routes'
|
||||
import { ModDetails, MuteLists } from 'types'
|
||||
import { extractModData, isModDataComplete, log, LogType } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
enum ModeratedFilterEnum {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
enum SearchingFilterEnum {
|
||||
Mods = 'Mods',
|
||||
Games = 'Games',
|
||||
Users = 'Users'
|
||||
}
|
||||
|
||||
interface FilterOptions {
|
||||
sort: SortByEnum
|
||||
moderated: ModeratedFilterEnum
|
||||
searching: SearchingFilterEnum
|
||||
}
|
||||
|
||||
export const SearchPage = () => {
|
||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
moderated: ModeratedFilterEnum.Moderated,
|
||||
searching: SearchingFilterEnum.Mods
|
||||
})
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [muteLists, setMuteLists] = useState<{
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
}>({
|
||||
admin: {
|
||||
authors: [],
|
||||
replaceableEvents: []
|
||||
},
|
||||
user: {
|
||||
authors: [],
|
||||
replaceableEvents: []
|
||||
}
|
||||
})
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
useDidMount(async () => {
|
||||
const pubkey = userState.user?.pubkey as string | undefined
|
||||
|
||||
const metadataController = await MetadataController.getInstance()
|
||||
metadataController.getMuteLists(pubkey).then((lists) => {
|
||||
setMuteLists(lists)
|
||||
})
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
||||
setSearchTerm(value)
|
||||
}
|
||||
|
||||
// Handle "Enter" key press inside the input
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='InnerBodyMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<div className='IBMSecMain'>
|
||||
<div className='SearchMainWrapper'>
|
||||
<div className='IBMSMTitleMain'>
|
||||
<h2 className='IBMSMTitleMainHeading'>
|
||||
Search:
|
||||
<span className='IBMSMTitleMainHeadingSpan'>
|
||||
{searchTerm}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className='SearchMain'>
|
||||
<div className='SearchMainInside'>
|
||||
<div className='SearchMainInsideWrapper'>
|
||||
<input
|
||||
type='text'
|
||||
className='SMIWInput'
|
||||
ref={searchTermRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder='Enter search term'
|
||||
/>
|
||||
<button
|
||||
className='btn btnMain SMIWButton'
|
||||
type='button'
|
||||
onClick={handleSearch}
|
||||
>
|
||||
<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>
|
||||
<Filters
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
{filterOptions.searching === SearchingFilterEnum.Mods && (
|
||||
<ModsResult
|
||||
searchTerm={searchTerm}
|
||||
filterOptions={filterOptions}
|
||||
muteLists={muteLists}
|
||||
/>
|
||||
)}
|
||||
{filterOptions.searching === SearchingFilterEnum.Users && (
|
||||
<UsersResult
|
||||
searchTerm={searchTerm}
|
||||
muteLists={muteLists}
|
||||
moderationFilter={filterOptions.moderated}
|
||||
/>
|
||||
)}
|
||||
{filterOptions.searching === SearchingFilterEnum.Games && (
|
||||
<GamesResult searchTerm={searchTerm} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type FiltersProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filters = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
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'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item, index) => (
|
||||
<div
|
||||
key={`sortByItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{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'
|
||||
>
|
||||
{filterOptions.moderated}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilterEnum).map((item, index) => {
|
||||
if (item === ModeratedFilterEnum.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (!isAdmin) return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`moderatedFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
moderated: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{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'
|
||||
>
|
||||
Searching: {filterOptions.searching}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SearchingFilterEnum).map((item, index) => (
|
||||
<div
|
||||
key={`searchingFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
searching: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const MAX_MODS_PER_PAGE = 10
|
||||
|
||||
type ModsResultProps = {
|
||||
filterOptions: FilterOptions
|
||||
searchTerm: string
|
||||
muteLists: {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
}
|
||||
}
|
||||
|
||||
const ModsResult = ({
|
||||
filterOptions,
|
||||
searchTerm,
|
||||
muteLists
|
||||
}: ModsResultProps) => {
|
||||
const hasEffectRun = useRef(false)
|
||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasEffectRun.current) {
|
||||
return
|
||||
}
|
||||
|
||||
hasEffectRun.current = true // Set it so the effect doesn't run again
|
||||
|
||||
const filter: Filter = {
|
||||
kinds: [kinds.ClassifiedListing],
|
||||
'#t': [T_TAG_VALUE]
|
||||
}
|
||||
|
||||
setIsSubscribing(true)
|
||||
|
||||
let subscriptions: Subscription[] = []
|
||||
|
||||
RelayController.getInstance()
|
||||
.subscribeForEvents(filter, [], (event) => {
|
||||
if (isModDataComplete(event)) {
|
||||
const mod = extractModData(event)
|
||||
setMods((prev) => [...prev, mod])
|
||||
}
|
||||
})
|
||||
.then((subs) => {
|
||||
subscriptions = subs
|
||||
})
|
||||
.catch((err) => {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'An error occurred in subscribing to relays.',
|
||||
err
|
||||
)
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsSubscribing(false)
|
||||
})
|
||||
|
||||
// Cleanup function to stop all subscriptions
|
||||
return () => {
|
||||
subscriptions.forEach((sub) => sub.close()) // close each subscription
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [searchTerm])
|
||||
|
||||
const filteredMods = useMemo(() => {
|
||||
if (searchTerm === '') return []
|
||||
|
||||
const lowerCaseSearchTerm = searchTerm.toLowerCase()
|
||||
|
||||
const filterFn = (mod: ModDetails) =>
|
||||
mod.title.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
mod.game.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
mod.summary.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
mod.body.toLowerCase().includes(lowerCaseSearchTerm) ||
|
||||
mod.tags.findIndex((tag) =>
|
||||
tag.toLowerCase().includes(lowerCaseSearchTerm)
|
||||
) > -1
|
||||
|
||||
return mods.filter(filterFn)
|
||||
}, [mods, searchTerm])
|
||||
|
||||
const filteredModList = useMemo(() => {
|
||||
let filtered: ModDetails[] = [...filteredMods]
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilterEnum.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilterEnum.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.user.authors.includes(mod.author) &&
|
||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [
|
||||
filteredMods,
|
||||
userState.user?.npub,
|
||||
filterOptions.sort,
|
||||
filterOptions.moderated,
|
||||
muteLists
|
||||
])
|
||||
|
||||
const handleNext = () => {
|
||||
setPage((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
setPage((prev) => prev - 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubscribing && (
|
||||
<LoadingSpinner desc='Subscribing to relays for mods' />
|
||||
)}
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMList'>
|
||||
{filteredModList
|
||||
.slice((page - 1) * MAX_MODS_PER_PAGE, page * MAX_MODS_PER_PAGE)
|
||||
.map((mod) => {
|
||||
const route = getModPageRoute(
|
||||
nip19.naddrEncode({
|
||||
identifier: mod.aTag,
|
||||
pubkey: mod.author,
|
||||
kind: kinds.ClassifiedListing
|
||||
})
|
||||
)
|
||||
|
||||
return (
|
||||
<ModCard
|
||||
key={mod.id}
|
||||
title={mod.title}
|
||||
gameName={mod.game}
|
||||
summary={mod.summary}
|
||||
imageUrl={mod.featuredImageUrl}
|
||||
route={route}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={filteredModList.length <= page * MAX_MODS_PER_PAGE}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type UsersResultProps = {
|
||||
searchTerm: string
|
||||
moderationFilter: ModeratedFilterEnum
|
||||
muteLists: {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
}
|
||||
}
|
||||
|
||||
const UsersResult = ({
|
||||
searchTerm,
|
||||
moderationFilter,
|
||||
muteLists
|
||||
}: UsersResultProps) => {
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [profiles, setProfiles] = useState<NDKUserProfile[]>([])
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTerm === '') {
|
||||
setProfiles([])
|
||||
} else {
|
||||
const filter: Filter = {
|
||||
kinds: [kinds.Metadata],
|
||||
search: searchTerm
|
||||
}
|
||||
|
||||
setIsFetching(true)
|
||||
RelayController.getInstance()
|
||||
.fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es'])
|
||||
.then((events) => {
|
||||
const results = events.map((event) => {
|
||||
const ndkEvent = new NDKEvent(undefined, event)
|
||||
const profile = profileFromEvent(ndkEvent)
|
||||
return profile
|
||||
})
|
||||
setProfiles(results)
|
||||
})
|
||||
.catch((err) => {
|
||||
log(true, LogType.Error, 'An error occurred in fetching users', err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsFetching(false)
|
||||
})
|
||||
}
|
||||
}, [searchTerm])
|
||||
|
||||
const filteredProfiles = useMemo(() => {
|
||||
let filtered = [...profiles]
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
moderationFilter === ModeratedFilterEnum.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(profile) => !muteLists.admin.authors.includes(profile.pubkey as string)
|
||||
)
|
||||
}
|
||||
|
||||
if (moderationFilter === ModeratedFilterEnum.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(profile) => !muteLists.user.authors.includes(profile.pubkey as string)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [userState.user?.npub, moderationFilter, profiles, muteLists])
|
||||
return (
|
||||
<>
|
||||
{isFetching && <LoadingSpinner desc='Fetching Profiles' />}
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMList'>
|
||||
{filteredProfiles.map((profile) => {
|
||||
if (profile.pubkey) {
|
||||
return (
|
||||
<ErrorBoundary key={profile.pubkey}>
|
||||
<Profile profile={profile} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type Game = {
|
||||
'Game Name': string
|
||||
'16 by 9 image': string
|
||||
'Boxart image': string
|
||||
}
|
||||
|
||||
const MAX_GAMES_PER_PAGE = 10
|
||||
|
||||
type GamesResultProps = {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
||||
const hasProcessedCSV = useRef(false)
|
||||
const [isProcessingCSVFile, setIsProcessingCSVFile] = useState(false)
|
||||
const [games, setGames] = useState<Game[]>([])
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasProcessedCSV.current) return
|
||||
hasProcessedCSV.current = true
|
||||
|
||||
setIsProcessingCSVFile(true)
|
||||
|
||||
// Fetch the CSV file from the public folder
|
||||
fetch('/assets/games.csv')
|
||||
.then((response) => response.text())
|
||||
.then((csvText) => {
|
||||
// Parse the CSV text using PapaParse
|
||||
Papa.parse<Game>(csvText, {
|
||||
worker: true,
|
||||
header: true,
|
||||
complete: (results) => {
|
||||
const uniqueGames: Game[] = []
|
||||
const gameNames = new Set<string>()
|
||||
|
||||
// Remove duplicate games based on 'Game Name'
|
||||
results.data.forEach((game) => {
|
||||
if (!gameNames.has(game['Game Name'])) {
|
||||
gameNames.add(game['Game Name'])
|
||||
uniqueGames.push(game)
|
||||
}
|
||||
})
|
||||
|
||||
// Set the unique games list
|
||||
setGames(uniqueGames)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
log(true, LogType.Error, 'Error occurred in processing csv file', err)
|
||||
toast.error(err.message || err)
|
||||
})
|
||||
.finally(() => {
|
||||
setIsProcessingCSVFile(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Reset the page to 1 whenever searchTerm changes
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [searchTerm])
|
||||
|
||||
const filteredGames = useMemo(() => {
|
||||
if (searchTerm === '') return []
|
||||
|
||||
const lowerCaseSearchTerm = searchTerm.toLowerCase()
|
||||
|
||||
return games.filter((game) =>
|
||||
game['Game Name'].toLowerCase().includes(lowerCaseSearchTerm)
|
||||
)
|
||||
}, [searchTerm, games])
|
||||
|
||||
const handleNext = () => {
|
||||
setPage((prev) => prev + 1)
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
setPage((prev) => prev - 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isProcessingCSVFile && <LoadingSpinner desc='Processing games file' />}
|
||||
<div className='IBMSecMain IBMSMListWrapper'>
|
||||
<div className='IBMSMList'>
|
||||
{filteredGames
|
||||
.slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE)
|
||||
.map((game) => (
|
||||
<GameCard
|
||||
key={game['Game Name']}
|
||||
title={game['Game Name']}
|
||||
imageUrl={game['Boxart image']}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
disabledNext={filteredGames.length <= page * MAX_GAMES_PER_PAGE}
|
||||
handlePrev={handlePrev}
|
||||
handleNext={handleNext}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { SearchPage } from 'pages/search'
|
||||
import { AboutPage } from '../pages/about'
|
||||
import { BlogsPage } from '../pages/blogs'
|
||||
import { GamesPage } from '../pages/games'
|
||||
@ -20,6 +21,7 @@ export const appRoutes = {
|
||||
submitMod: '/submit-mod',
|
||||
editMod: '/edit-mod/:naddr',
|
||||
write: '/write',
|
||||
search: '/search',
|
||||
settingsProfile: '/settings-profile',
|
||||
settingsRelays: '/settings-relays',
|
||||
settingsPreferences: '/settings-preferences',
|
||||
@ -77,6 +79,10 @@ export const routes = [
|
||||
path: appRoutes.write,
|
||||
element: <WritePage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.search,
|
||||
element: <SearchPage />
|
||||
},
|
||||
{
|
||||
path: appRoutes.settingsProfile,
|
||||
element: <SettingsPage />
|
||||
|
@ -102,14 +102,17 @@
|
||||
@media (max-width: 992px) {
|
||||
.swiper-slide.IBMSMSliderContainerWrapperSlider {
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
padding: 0 0 25px 0;
|
||||
padding: 0 5px 25px 5px;
|
||||
grid-gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.swiper-slide.IBMSMSliderContainerWrapperSlider {
|
||||
grid-template-columns: 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-gap: 15px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +217,12 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.IBMSMSCWSPicWrapper {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.IBMSMSCWSInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -234,7 +243,7 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.IBMSMSCWSInfo {
|
||||
/*margin: -25px 10px 0 10px;*/
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,6 +265,10 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.IBMSMSCWSInfoTextWrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.IBMSMSCWSInfoText {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@ -264,7 +277,6 @@
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
|
@ -145,10 +145,11 @@
|
||||
}
|
||||
|
||||
.IBMSMSMSSS_Author_Top_Name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: rgba(255,255,255,0.75);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.IBMSMSMSSS_Author_Top_PPWrapper {
|
||||
@ -160,6 +161,15 @@
|
||||
|
||||
.IBMSMSMSSS_Author_Top_Handle {
|
||||
color: rgba(255,255,255,0.5);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
.IBMSMSMSSS_Author_Top_Handle.IBMSMSMSSS_Author_Top_HandleNomen {
|
||||
color: #F7931A;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.IBMSMSMSSS_Author_Top_NostrLinksLink.IBMSMSMSSS_A_T_NLL_IBMSMSMSSSFollow {
|
||||
|
@ -26,6 +26,7 @@
|
||||
}
|
||||
|
||||
.cardBlogMainInside {
|
||||
transition: ease 0.4s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
@ -36,5 +37,12 @@
|
||||
justify-content: end;
|
||||
align-items: start;
|
||||
padding: 15px;
|
||||
background: linear-gradient(rgb(0 0 0 / 0%) 0%, rgb(0 0 0 / 75%) 100%);
|
||||
}
|
||||
|
||||
.cardBlogMainInside:hover {
|
||||
transition: ease 0.4s;
|
||||
background: linear-gradient(rgb(0 0 0 / 35%) 0%, rgb(0 0 0 / 85%) 100%);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
|
@ -4,7 +4,7 @@
|
||||
grid-gap: 25px;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
@media (max-width: 1200px) {
|
||||
.IBMSMSplitMain {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -53,7 +53,6 @@
|
||||
align-items: center;
|
||||
padding: 25px;
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.popUpMainCardBottomQR {
|
||||
|
121
src/styles/socialNav.css
Normal file
121
src/styles/socialNav.css
Normal file
@ -0,0 +1,121 @@
|
||||
.socialNav {
|
||||
transition: ease 0.4s;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.socialNav {
|
||||
transition: ease 0.4s;
|
||||
right: 100%;
|
||||
transform: translateX(100%);
|
||||
width: 100%;
|
||||
align-items: end;
|
||||
}
|
||||
}
|
||||
|
||||
.socialNavInside {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
Background: linear-gradient(to top right, rgba(27,27,27,0.9), rgba(35,35,35,0.9), rgba(27,27,27,0.9));
|
||||
box-shadow: 0 0 8px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-gap: 5px;
|
||||
border: solid 2px rgba(255,255,255,0.05);
|
||||
overflow-x: auto;
|
||||
max-width: 80vw;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.socialNavInside {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.socialNavInside::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.btnMain.socialNavInsideBtn {
|
||||
transition: ease 0.4s;
|
||||
padding: 0 15px;
|
||||
font-size: 24px;
|
||||
height: 45px;
|
||||
width: 55px;
|
||||
border-radius: 10px;
|
||||
Background: linear-gradient(to top right, rgba(50,50,50,0), rgba(55,55,55,0), rgba(50,50,50,0));
|
||||
color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
.btnMain.socialNavInsideBtn:hover {
|
||||
transition: ease 0.4s;
|
||||
background: #434343;
|
||||
}
|
||||
|
||||
.btnMain.socialNavInsideBtn.socialNavInsideBtnActive {
|
||||
Background: #434343;
|
||||
box-shadow: 0 0 8px 0 rgb(0,0,0,0.1);
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
.socialNavInsideWrapper {
|
||||
margin: 10px 0 15px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
grid-gap: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.socialNavInsideWrapper {
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
||||
.socialNavCollapse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
Background: linear-gradient(to top right, rgba(27,27,27,0.75), rgba(35,35,35,0.75), rgba(27,27,27,0.75));
|
||||
box-shadow: 0 0 8px rgba(0,0,0,0.2);
|
||||
padding: 15px 5px;
|
||||
border-radius: 5px;
|
||||
border: solid 1px rgba(255,255,255,0.05);
|
||||
backdrop-filter: blur(5px);
|
||||
cursor: pointer;
|
||||
transform: scale(1);
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
.socialNavCollapse:hover {
|
||||
Background: linear-gradient(rgba(255,255,255,0.01), rgba(255,255,255,0.01)), linear-gradient(to right top, rgba(27,27,27,0.75), rgba(35,35,35,0.75), rgba(27,27,27,0.75));
|
||||
color: rgb(255,255,255);
|
||||
}
|
||||
|
||||
.socialNavCollapseIcon {
|
||||
transition: ease 0.4s;
|
||||
}
|
||||
|
||||
.btnMain.socialNavInsideBtn::before {
|
||||
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);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btnMain.socialNavInsideBtn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user