Compare commits
No commits in common. "d01521a5f02ad00235e441e17c6877bc6fa7149e" and "9cb3d2fb63a26110f9d3d0cf1c40902f7a20fb3a" have entirely different histories.
d01521a5f0
...
9cb3d2fb63
Can't render this file because it is too large.
|
@ -1,3 +0,0 @@
|
|||||||
Game Name,16 by 9 image,Boxart image
|
|
||||||
Voices of the Void,,https://image.nostr.build/472949882d0756c84d3effd9f641b10c88abd48265f0f01f360937b189d50b54.jpg
|
|
||||||
Shroom and Gloom,,
|
|
|
@ -1,4 +0,0 @@
|
|||||||
Game Name,16 by 9 image,Boxart image
|
|
||||||
Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
|
|
||||||
Vintage Story,,
|
|
||||||
Yandere Simulator,,
|
|
|
@ -1,7 +1,5 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
|
||||||
import '../styles/cardGames.css'
|
import '../styles/cardGames.css'
|
||||||
import { handleGameImageError } from '../utils'
|
import { handleGameImageError } from '../utils'
|
||||||
import { getGamePageRoute } from 'routes'
|
|
||||||
|
|
||||||
type GameCardProps = {
|
type GameCardProps = {
|
||||||
title: string
|
title: string
|
||||||
@ -9,13 +7,8 @@ type GameCardProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GameCard = ({ title, imageUrl }: GameCardProps) => {
|
export const GameCard = ({ title, imageUrl }: GameCardProps) => {
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<a className='cardGameMainWrapperLink' href='search.html'>
|
||||||
className='cardGameMainWrapperLink'
|
|
||||||
onClick={() => navigate(getGamePageRoute(title))}
|
|
||||||
>
|
|
||||||
<div className='cardGameMainWrapper'>
|
<div className='cardGameMainWrapper'>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
@ -26,6 +19,6 @@ export const GameCard = ({ title, imageUrl }: GameCardProps) => {
|
|||||||
<div className='cardGameMainTitle'>
|
<div className='cardGameMainTitle'>
|
||||||
<p>{title}</p>
|
<p>{title}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||||
|
import Papa from 'papaparse'
|
||||||
import React, {
|
import React, {
|
||||||
Fragment,
|
Fragment,
|
||||||
useCallback,
|
useCallback,
|
||||||
@ -8,16 +9,11 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { FixedSizeList as List } from 'react-window'
|
import { FixedSizeList as List } from 'react-window'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { T_TAG_VALUE } from '../constants'
|
import { useAppSelector } from '../hooks'
|
||||||
import { RelayController } from '../controllers'
|
|
||||||
import { useAppSelector, useGames } from '../hooks'
|
|
||||||
import { appRoutes, getModPageRoute } from '../routes'
|
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { DownloadUrl, ModDetails, ModFormState } from '../types'
|
|
||||||
import {
|
import {
|
||||||
initializeFormState,
|
initializeFormState,
|
||||||
isReachable,
|
isReachable,
|
||||||
@ -28,7 +24,12 @@ import {
|
|||||||
now
|
now
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { CheckboxField, InputError, InputField } from './Inputs'
|
import { CheckboxField, InputError, InputField } from './Inputs'
|
||||||
|
import { RelayController } from '../controllers'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { appRoutes, getModPageRoute } from '../routes'
|
||||||
|
import { DownloadUrl, ModFormState, ModDetails } from '../types'
|
||||||
import { LoadingSpinner } from './LoadingSpinner'
|
import { LoadingSpinner } from './LoadingSpinner'
|
||||||
|
import { T_TAG_VALUE } from '../constants'
|
||||||
|
|
||||||
interface FormErrors {
|
interface FormErrors {
|
||||||
game?: string
|
game?: string
|
||||||
@ -47,6 +48,8 @@ interface GameOption {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let processedCSV = false
|
||||||
|
|
||||||
type ModFormProps = {
|
type ModFormProps = {
|
||||||
existingModData?: ModDetails
|
existingModData?: ModDetails
|
||||||
}
|
}
|
||||||
@ -54,7 +57,6 @@ type ModFormProps = {
|
|||||||
export const ModForm = ({ existingModData }: ModFormProps) => {
|
export const ModForm = ({ existingModData }: ModFormProps) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const games = useGames()
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
const [isPublishing, setIsPublishing] = useState(false)
|
const [isPublishing, setIsPublishing] = useState(false)
|
||||||
@ -62,7 +64,6 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
const [formState, setFormState] = useState<ModFormState>(
|
const [formState, setFormState] = useState<ModFormState>(
|
||||||
initializeFormState(existingModData)
|
initializeFormState(existingModData)
|
||||||
)
|
)
|
||||||
const [formErrors, setFormErrors] = useState<FormErrors>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.pathname === appRoutes.submitMod) {
|
if (location.pathname === appRoutes.submitMod) {
|
||||||
@ -70,13 +71,35 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
}
|
}
|
||||||
}, [location.pathname]) // Only trigger when the pathname changes to submit-mod
|
}, [location.pathname]) // Only trigger when the pathname changes to submit-mod
|
||||||
|
|
||||||
|
const [formErrors, setFormErrors] = useState<FormErrors>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const options = games.map((game) => ({
|
if (processedCSV) return
|
||||||
label: game['Game Name'],
|
processedCSV = true
|
||||||
value: game['Game Name']
|
|
||||||
|
// 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 Name': string
|
||||||
|
'16 by 9 image': string
|
||||||
|
'Boxart image': string
|
||||||
|
}>(csvText, {
|
||||||
|
worker: true,
|
||||||
|
header: true,
|
||||||
|
complete: (results) => {
|
||||||
|
const options = results.data.map((row) => ({
|
||||||
|
label: row['Game Name'],
|
||||||
|
value: row['Game Name']
|
||||||
}))
|
}))
|
||||||
setGameOptions(options)
|
setGameOptions(options)
|
||||||
}, [games])
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error fetching CSV file:', error))
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleInputChange = useCallback((name: string, value: string) => {
|
const handleInputChange = useCallback((name: string, value: string) => {
|
||||||
setFormState((prevState) => ({
|
setFormState((prevState) => ({
|
||||||
|
@ -38,101 +38,3 @@ export const Pagination = React.memo(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type PaginationWithPageNumbersProps = {
|
|
||||||
currentPage: number
|
|
||||||
totalPages: number
|
|
||||||
handlePageChange: (page: number) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PaginationWithPageNumbers = ({
|
|
||||||
currentPage,
|
|
||||||
totalPages,
|
|
||||||
handlePageChange
|
|
||||||
}: PaginationWithPageNumbersProps) => {
|
|
||||||
// Function to render the pagination controls with page numbers
|
|
||||||
const renderPagination = () => {
|
|
||||||
const pagesToShow = 5 // Number of page numbers to show around the current page
|
|
||||||
const pageNumbers: (number | string)[] = [] // Array to store page numbers and ellipses
|
|
||||||
|
|
||||||
// Case when the total number of pages is less than or equal to the limit
|
|
||||||
if (totalPages <= pagesToShow + 2) {
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
pageNumbers.push(i) // Add all pages to the pagination
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Add the first page (always visible)
|
|
||||||
pageNumbers.push(1)
|
|
||||||
|
|
||||||
// Calculate the range of pages to show around the current page
|
|
||||||
const startPage = Math.max(2, currentPage - Math.floor(pagesToShow / 2))
|
|
||||||
const endPage = Math.min(
|
|
||||||
totalPages - 1,
|
|
||||||
currentPage + Math.floor(pagesToShow / 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add ellipsis if there are pages between the first page and the startPage
|
|
||||||
if (startPage > 2) pageNumbers.push('...')
|
|
||||||
|
|
||||||
// Add the pages around the current page
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
pageNumbers.push(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ellipsis if there are pages between the endPage and the last page
|
|
||||||
if (endPage < totalPages - 1) pageNumbers.push('...')
|
|
||||||
|
|
||||||
// Add the last page (always visible)
|
|
||||||
pageNumbers.push(totalPages)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map over the array and render each page number or ellipsis
|
|
||||||
return pageNumbers.map((page, index) => {
|
|
||||||
if (typeof page === 'number') {
|
|
||||||
// For actual page numbers, render clickable boxes
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`PaginationMainInsideBox ${
|
|
||||||
currentPage === page ? 'PMIBActive' : '' // Highlight the current page
|
|
||||||
}`}
|
|
||||||
onClick={() => handlePageChange(page)} // Navigate to the selected page
|
|
||||||
>
|
|
||||||
<p>{page}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// For ellipses, render non-clickable dots
|
|
||||||
return (
|
|
||||||
<p key={index} className='PaginationMainInsideBox PMIBDots'>
|
|
||||||
...
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='IBMSecMain'>
|
|
||||||
<div className='PaginationMain'>
|
|
||||||
<div className='PaginationMainInside'>
|
|
||||||
<div
|
|
||||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
|
||||||
onClick={() => handlePageChange(currentPage - 1)}
|
|
||||||
>
|
|
||||||
<i className='fas fa-chevron-left'></i>
|
|
||||||
</div>
|
|
||||||
<div className='PaginationMainInsideBoxGroup'>
|
|
||||||
{renderPagination()}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
|
|
||||||
onClick={() => handlePageChange(currentPage + 1)}
|
|
||||||
>
|
|
||||||
<i className='fas fa-chevron-right'></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
@ -43,93 +43,14 @@ export const LANDING_PAGE_DATA = {
|
|||||||
// Both of these arrays can have separate items
|
// Both of these arrays can have separate items
|
||||||
export const REACTIONS = {
|
export const REACTIONS = {
|
||||||
positive: {
|
positive: {
|
||||||
emojis: [
|
emojis: ['+', '❤️', '💙', '💖', '💚','⭐', '🚀', '🫂', '🎉', '🥳', '🎊', '👍', '💪', '😎'],
|
||||||
'+',
|
shortCodes: [':red_heart:', ':blue_heart:', ':sparkling_heart:', ':green_heart:', ':star:', ':rocket:', ':people_hugging:', ':party_popper:',
|
||||||
'❤️',
|
':tada:', ':partying_face:', ':confetti_ball:', ':thumbs_up:', ':+1:', ':thumbsup:', ':thumbup:', ':flexed_biceps:', ':muscle:']
|
||||||
'💙',
|
|
||||||
'💖',
|
|
||||||
'💚',
|
|
||||||
'⭐',
|
|
||||||
'🚀',
|
|
||||||
'🫂',
|
|
||||||
'🎉',
|
|
||||||
'🥳',
|
|
||||||
'🎊',
|
|
||||||
'👍',
|
|
||||||
'💪',
|
|
||||||
'😎'
|
|
||||||
],
|
|
||||||
shortCodes: [
|
|
||||||
':red_heart:',
|
|
||||||
':blue_heart:',
|
|
||||||
':sparkling_heart:',
|
|
||||||
':green_heart:',
|
|
||||||
':star:',
|
|
||||||
':rocket:',
|
|
||||||
':people_hugging:',
|
|
||||||
':party_popper:',
|
|
||||||
':tada:',
|
|
||||||
':partying_face:',
|
|
||||||
':confetti_ball:',
|
|
||||||
':thumbs_up:',
|
|
||||||
':+1:',
|
|
||||||
':thumbsup:',
|
|
||||||
':thumbup:',
|
|
||||||
':flexed_biceps:',
|
|
||||||
':muscle:'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
negative: {
|
negative: {
|
||||||
emojis: [
|
emojis: ['-', '💩', '💔', '👎', '😠', '😞', '🤬', '🤢', '🤮', '🖕', '😡', '💢', '😠', '💀'],
|
||||||
'-',
|
shortCodes: [':poop:', ':shit:', ':poo:', ':hankey:', ':pile_of_poo:', ':broken_heart:', ':thumbsdown:', ':thumbdown:', ':nauseated_face:', ':sick:',
|
||||||
'💩',
|
':face_vomiting:', ':vomiting_face:', ':face_with_open_mouth_vomiting:', ':middle_finger:', ':rage:', ':anger:', ':anger_symbol:', ':angry_face:', ':angry:',
|
||||||
'💔',
|
':smiling_face_with_sunglasses:', ':sunglasses:', ':skull:', ':skeleton:']
|
||||||
'👎',
|
|
||||||
'😠',
|
|
||||||
'😞',
|
|
||||||
'🤬',
|
|
||||||
'🤢',
|
|
||||||
'🤮',
|
|
||||||
'🖕',
|
|
||||||
'😡',
|
|
||||||
'💢',
|
|
||||||
'😠',
|
|
||||||
'💀'
|
|
||||||
],
|
|
||||||
shortCodes: [
|
|
||||||
':poop:',
|
|
||||||
':shit:',
|
|
||||||
':poo:',
|
|
||||||
':hankey:',
|
|
||||||
':pile_of_poo:',
|
|
||||||
':broken_heart:',
|
|
||||||
':thumbsdown:',
|
|
||||||
':thumbdown:',
|
|
||||||
':nauseated_face:',
|
|
||||||
':sick:',
|
|
||||||
':face_vomiting:',
|
|
||||||
':vomiting_face:',
|
|
||||||
':face_with_open_mouth_vomiting:',
|
|
||||||
':middle_finger:',
|
|
||||||
':rage:',
|
|
||||||
':anger:',
|
|
||||||
':anger_symbol:',
|
|
||||||
':angry_face:',
|
|
||||||
':angry:',
|
|
||||||
':smiling_face_with_sunglasses:',
|
|
||||||
':sunglasses:',
|
|
||||||
':skull:',
|
|
||||||
':skeleton:'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: there should be a corresponding CSV file in public/assets/games folder for each entry in the array
|
|
||||||
export const GAME_FILES = [
|
|
||||||
'Games_Itch.csv',
|
|
||||||
'Games_Other.csv',
|
|
||||||
'Games_Steam.csv'
|
|
||||||
]
|
|
||||||
|
|
||||||
export const MAX_MODS_PER_PAGE = 10
|
|
||||||
export const MAX_GAMES_PER_PAGE = 10
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
export * from './redux'
|
export * from './redux'
|
||||||
export * from './useDidMount'
|
export * from './useDidMount'
|
||||||
export * from './useGames'
|
|
||||||
export * from './useMuteLists'
|
|
||||||
export * from './useReactions'
|
export * from './useReactions'
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import { GAME_FILES } from 'constants.ts'
|
|
||||||
import Papa from 'papaparse'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { Game } from 'types'
|
|
||||||
import { log, LogType } from 'utils'
|
|
||||||
|
|
||||||
export const useGames = () => {
|
|
||||||
const hasProcessedFiles = useRef(false)
|
|
||||||
const [games, setGames] = useState<Game[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasProcessedFiles.current) return
|
|
||||||
|
|
||||||
hasProcessedFiles.current = true
|
|
||||||
|
|
||||||
const readGamesCSVs = async () => {
|
|
||||||
const uniqueGames: Game[] = []
|
|
||||||
const gameNames = new Set<string>()
|
|
||||||
|
|
||||||
// Function to promisify PapaParse
|
|
||||||
const parseCSV = (csvText: string) =>
|
|
||||||
new Promise<Game[]>((resolve, reject) => {
|
|
||||||
Papa.parse<Game>(csvText, {
|
|
||||||
worker: true,
|
|
||||||
header: true,
|
|
||||||
complete: (results) => {
|
|
||||||
if (results.errors.length) {
|
|
||||||
reject(results.errors)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(results.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch and parse each file
|
|
||||||
const promises = GAME_FILES.map(async (filename) => {
|
|
||||||
const response = await fetch(`/assets/games/${filename}`)
|
|
||||||
const csvText = await response.text()
|
|
||||||
const parsedGames = await parseCSV(csvText)
|
|
||||||
|
|
||||||
// Remove duplicate games based on 'Game Name'
|
|
||||||
parsedGames.forEach((game) => {
|
|
||||||
if (!gameNames.has(game['Game Name'])) {
|
|
||||||
gameNames.add(game['Game Name'])
|
|
||||||
uniqueGames.push(game)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
setGames(uniqueGames)
|
|
||||||
} catch (err) {
|
|
||||||
log(
|
|
||||||
true,
|
|
||||||
LogType.Error,
|
|
||||||
'An error occurred in reading and parsing games CSVs',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle the unknown error type
|
|
||||||
if (err instanceof Error) {
|
|
||||||
toast.error(err.message)
|
|
||||||
} else if (Array.isArray(err) && err.length > 0 && err[0]?.message) {
|
|
||||||
// Handle the case when it's an array of PapaParse errors
|
|
||||||
toast.error(err[0].message)
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
'An unknown error occurred in reading and parsing csv files'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readGamesCSVs()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return games
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { MuteLists } from 'types'
|
|
||||||
import { useAppSelector } from './redux'
|
|
||||||
import { MetadataController } from 'controllers'
|
|
||||||
|
|
||||||
export const useMuteLists = () => {
|
|
||||||
const [muteLists, setMuteLists] = useState<{
|
|
||||||
admin: MuteLists
|
|
||||||
user: MuteLists
|
|
||||||
}>({
|
|
||||||
admin: {
|
|
||||||
authors: [],
|
|
||||||
replaceableEvents: []
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
authors: [],
|
|
||||||
replaceableEvents: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getMuteLists = async () => {
|
|
||||||
const pubkey = userState.user?.pubkey as string | undefined
|
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
metadataController.getMuteLists(pubkey).then((lists) => {
|
|
||||||
setMuteLists(lists)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getMuteLists()
|
|
||||||
}, [userState])
|
|
||||||
|
|
||||||
return muteLists
|
|
||||||
}
|
|
@ -1,299 +0,0 @@
|
|||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
|
||||||
import { ModCard } from 'components/ModCard'
|
|
||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
|
||||||
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
|
|
||||||
import { RelayController } from 'controllers'
|
|
||||||
import { useAppSelector, useMuteLists } from 'hooks'
|
|
||||||
import { Filter, kinds, nip19 } from 'nostr-tools'
|
|
||||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
|
||||||
import React, {
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import { useParams } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { getModPageRoute } from 'routes'
|
|
||||||
import { ModDetails } 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'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterOptions {
|
|
||||||
sort: SortByEnum
|
|
||||||
moderated: ModeratedFilterEnum
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GamePage = () => {
|
|
||||||
const params = useParams()
|
|
||||||
const { name: gameName } = params
|
|
||||||
const muteLists = useMuteLists()
|
|
||||||
|
|
||||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
|
||||||
sort: SortByEnum.Latest,
|
|
||||||
moderated: ModeratedFilterEnum.Moderated
|
|
||||||
})
|
|
||||||
const [mods, setMods] = useState<ModDetails[]>([])
|
|
||||||
|
|
||||||
const hasEffectRun = useRef(false)
|
|
||||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
|
||||||
|
|
||||||
const filteredMods = useMemo(() => {
|
|
||||||
let filtered: ModDetails[] = [...mods]
|
|
||||||
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
|
|
||||||
}, [
|
|
||||||
mods,
|
|
||||||
userState.user?.npub,
|
|
||||||
filterOptions.sort,
|
|
||||||
filterOptions.moderated,
|
|
||||||
muteLists
|
|
||||||
])
|
|
||||||
|
|
||||||
// Pagination logic
|
|
||||||
const totalGames = filteredMods.length
|
|
||||||
const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE)
|
|
||||||
const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE
|
|
||||||
const endIndex = startIndex + MAX_MODS_PER_PAGE
|
|
||||||
const currentMods = filteredMods.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
if (page >= 1 && page <= totalPages) {
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if (mod.game === gameName) 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
|
|
||||||
}
|
|
||||||
}, [gameName])
|
|
||||||
|
|
||||||
if (!gameName) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isSubscribing && (
|
|
||||||
<LoadingSpinner desc='Subscribing to relays for mods' />
|
|
||||||
)}
|
|
||||||
<div className='InnerBodyMain'>
|
|
||||||
<div className='ContainerMain'>
|
|
||||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
|
||||||
<div className='IBMSecMain'>
|
|
||||||
<div className='SearchMainWrapper'>
|
|
||||||
<div className='IBMSMTitleMain'>
|
|
||||||
<h2 className='IBMSMTitleMainHeading'>
|
|
||||||
Game:
|
|
||||||
<span className='IBMSMTitleMainHeadingSpan'>
|
|
||||||
{gameName}
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Filters
|
|
||||||
filterOptions={filterOptions}
|
|
||||||
setFilterOptions={setFilterOptions}
|
|
||||||
/>
|
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
|
||||||
<div className='IBMSMList'>
|
|
||||||
{currentMods.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>
|
|
||||||
<PaginationWithPageNumbers
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
handlePageChange={handlePageChange}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
@ -1,51 +1,9 @@
|
|||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
|
||||||
import { MAX_GAMES_PER_PAGE } from 'constants.ts'
|
|
||||||
import { useGames } from 'hooks'
|
|
||||||
import { useRef, useState } from 'react'
|
|
||||||
import { GameCard } from '../components/GameCard'
|
|
||||||
import '../styles/pagination.css'
|
import '../styles/pagination.css'
|
||||||
import '../styles/search.css'
|
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { createSearchParams, useNavigate } from 'react-router-dom'
|
import '../styles/search.css'
|
||||||
import { appRoutes } from 'routes'
|
import { GameCard } from '../components/GameCard'
|
||||||
|
|
||||||
export const GamesPage = () => {
|
export const GamesPage = () => {
|
||||||
const navigate = useNavigate()
|
|
||||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
|
||||||
const games = useGames()
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
|
|
||||||
// Pagination logic
|
|
||||||
const totalGames = games.length
|
|
||||||
const totalPages = Math.ceil(totalGames / MAX_GAMES_PER_PAGE)
|
|
||||||
const startIndex = (currentPage - 1) * MAX_GAMES_PER_PAGE
|
|
||||||
const endIndex = startIndex + MAX_GAMES_PER_PAGE
|
|
||||||
const currentGames = games.slice(startIndex, endIndex)
|
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
if (page >= 1 && page <= totalPages) {
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
|
||||||
if (value !== '') {
|
|
||||||
const searchParams = createSearchParams({
|
|
||||||
searchTerm: value,
|
|
||||||
searching: 'Games'
|
|
||||||
})
|
|
||||||
navigate({ pathname: appRoutes.search, search: `?${searchParams}` })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle "Enter" key press inside the input
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
handleSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
@ -53,23 +11,13 @@ export const GamesPage = () => {
|
|||||||
<div className='IBMSecMain'>
|
<div className='IBMSecMain'>
|
||||||
<div className='SearchMainWrapper'>
|
<div className='SearchMainWrapper'>
|
||||||
<div className='IBMSMTitleMain'>
|
<div className='IBMSMTitleMain'>
|
||||||
<h2 className='IBMSMTitleMainHeading'>Games</h2>
|
<h2 className='IBMSMTitleMainHeading'>Games (WIP)</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='SearchMain'>
|
<div className='SearchMain'>
|
||||||
<div className='SearchMainInside'>
|
<div className='SearchMainInside'>
|
||||||
<div className='SearchMainInsideWrapper'>
|
<div className='SearchMainInsideWrapper'>
|
||||||
<input
|
<input type='text' className='SMIWInput' />
|
||||||
type='text'
|
<button className='btn btnMain SMIWButton' type='button'>
|
||||||
className='SMIWInput'
|
|
||||||
ref={searchTermRef}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder='Enter search term'
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className='btn btnMain SMIWButton'
|
|
||||||
type='button'
|
|
||||||
onClick={handleSearch}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
viewBox='0 0 512 512'
|
viewBox='0 0 512 512'
|
||||||
@ -87,20 +35,61 @@ export const GamesPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMList IBMSMListFeaturedAlt'>
|
<div className='IBMSMList IBMSMListFeaturedAlt'>
|
||||||
{currentGames.map((game) => (
|
|
||||||
<GameCard
|
<GameCard
|
||||||
key={game['Game Name']}
|
title='This is a game title, the best game title'
|
||||||
title={game['Game Name']}
|
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
imageUrl={game['Boxart image']}
|
/>
|
||||||
|
<GameCard
|
||||||
|
title='This is a game title, the best game title'
|
||||||
|
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
|
/>
|
||||||
|
<GameCard
|
||||||
|
title='This is a game title, the best game title'
|
||||||
|
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
|
/>
|
||||||
|
<GameCard
|
||||||
|
title='This is a game title, the best game title'
|
||||||
|
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
|
/>
|
||||||
|
<GameCard
|
||||||
|
title='This is a game title, the best game title'
|
||||||
|
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PaginationWithPageNumbers
|
<div className='IBMSecMain'>
|
||||||
currentPage={currentPage}
|
<div className='PaginationMain'>
|
||||||
totalPages={totalPages}
|
<div className='PaginationMainInside'>
|
||||||
handlePageChange={handlePageChange}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Pagination } from 'components/Pagination'
|
|
||||||
import { kinds, nip19 } from 'nostr-tools'
|
import { kinds, nip19 } from 'nostr-tools'
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@ -10,16 +9,17 @@ import React, {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
import { ModCard } from '../components/ModCard'
|
import { ModCard } from '../components/ModCard'
|
||||||
import { MOD_FILTER_LIMIT } from '../constants'
|
|
||||||
import { MetadataController } from '../controllers'
|
import { MetadataController } from '../controllers'
|
||||||
import { useAppSelector, useDidMount, useMuteLists } from '../hooks'
|
import { useAppSelector, useDidMount } from '../hooks'
|
||||||
import { getModPageRoute } from '../routes'
|
import { getModPageRoute } from '../routes'
|
||||||
import '../styles/filters.css'
|
import '../styles/filters.css'
|
||||||
import '../styles/pagination.css'
|
import '../styles/pagination.css'
|
||||||
import '../styles/search.css'
|
import '../styles/search.css'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { ModDetails } from '../types'
|
import { ModDetails, MuteLists } from '../types'
|
||||||
import { fetchMods } from '../utils'
|
import { fetchMods } from '../utils'
|
||||||
|
import { MOD_FILTER_LIMIT } from '../constants'
|
||||||
|
import { Pagination } from 'components/Pagination'
|
||||||
|
|
||||||
enum SortBy {
|
enum SortBy {
|
||||||
Latest = 'Latest',
|
Latest = 'Latest',
|
||||||
@ -56,8 +56,19 @@ export const ModsPage = () => {
|
|||||||
source: window.location.host,
|
source: window.location.host,
|
||||||
moderated: ModeratedFilter.Moderated
|
moderated: ModeratedFilter.Moderated
|
||||||
})
|
})
|
||||||
const muteLists = useMuteLists()
|
const [muteLists, setMuteLists] = useState<{
|
||||||
|
admin: MuteLists
|
||||||
|
user: MuteLists
|
||||||
|
}>({
|
||||||
|
admin: {
|
||||||
|
authors: [],
|
||||||
|
replaceableEvents: []
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
authors: [],
|
||||||
|
replaceableEvents: []
|
||||||
|
}
|
||||||
|
})
|
||||||
const [nsfwList, setNSFWList] = useState<string[]>([])
|
const [nsfwList, setNSFWList] = useState<string[]>([])
|
||||||
|
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
@ -65,7 +76,12 @@ export const ModsPage = () => {
|
|||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
useDidMount(async () => {
|
useDidMount(async () => {
|
||||||
|
const pubkey = userState.user?.pubkey as string | undefined
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
const metadataController = await MetadataController.getInstance()
|
||||||
|
metadataController.getMuteLists(pubkey).then((lists) => {
|
||||||
|
setMuteLists(lists)
|
||||||
|
})
|
||||||
|
|
||||||
metadataController.getNSFWList().then((list) => {
|
metadataController.getNSFWList().then((list) => {
|
||||||
setNSFWList(list)
|
setNSFWList(list)
|
||||||
|
@ -5,15 +5,12 @@ import { LoadingSpinner } from 'components/LoadingSpinner'
|
|||||||
import { ModCard } from 'components/ModCard'
|
import { ModCard } from 'components/ModCard'
|
||||||
import { Pagination } from 'components/Pagination'
|
import { Pagination } from 'components/Pagination'
|
||||||
import { Profile } from 'components/ProfileSection'
|
import { Profile } from 'components/ProfileSection'
|
||||||
import {
|
import { T_TAG_VALUE } from 'constants.ts'
|
||||||
MAX_GAMES_PER_PAGE,
|
import { MetadataController, RelayController } from 'controllers'
|
||||||
MAX_MODS_PER_PAGE,
|
import { useAppSelector, useDidMount } from 'hooks'
|
||||||
T_TAG_VALUE
|
|
||||||
} from 'constants.ts'
|
|
||||||
import { RelayController } from 'controllers'
|
|
||||||
import { useAppSelector, useGames, useMuteLists } from 'hooks'
|
|
||||||
import { Filter, kinds, nip19 } from 'nostr-tools'
|
import { Filter, kinds, nip19 } from 'nostr-tools'
|
||||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
import { Subscription } from 'nostr-tools/abstract-relay'
|
||||||
|
import Papa from 'papaparse'
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
@ -22,7 +19,6 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { getModPageRoute } from 'routes'
|
import { getModPageRoute } from 'routes'
|
||||||
import { ModDetails, MuteLists } from 'types'
|
import { ModDetails, MuteLists } from 'types'
|
||||||
@ -54,20 +50,37 @@ interface FilterOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SearchPage = () => {
|
export const SearchPage = () => {
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
|
|
||||||
const muteLists = useMuteLists()
|
|
||||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||||
sort: SortByEnum.Latest,
|
sort: SortByEnum.Latest,
|
||||||
moderated: ModeratedFilterEnum.Moderated,
|
moderated: ModeratedFilterEnum.Moderated,
|
||||||
searching:
|
searching: SearchingFilterEnum.Mods
|
||||||
(searchParams.get('searching') as SearchingFilterEnum) ||
|
})
|
||||||
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 [searchTerm, setSearchTerm] = useState(
|
|
||||||
searchParams.get('searchTerm') || ''
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
const value = searchTermRef.current?.value || '' // Access the input value from the ref
|
||||||
@ -265,6 +278,8 @@ const Filters = React.memo(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const MAX_MODS_PER_PAGE = 10
|
||||||
|
|
||||||
type ModsResultProps = {
|
type ModsResultProps = {
|
||||||
filterOptions: FilterOptions
|
filterOptions: FilterOptions
|
||||||
searchTerm: string
|
searchTerm: string
|
||||||
@ -529,14 +544,64 @@ const UsersResult = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Game = {
|
||||||
|
'Game Name': string
|
||||||
|
'16 by 9 image': string
|
||||||
|
'Boxart image': string
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_GAMES_PER_PAGE = 10
|
||||||
|
|
||||||
type GamesResultProps = {
|
type GamesResultProps = {
|
||||||
searchTerm: string
|
searchTerm: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
||||||
const games = useGames()
|
const hasProcessedCSV = useRef(false)
|
||||||
|
const [isProcessingCSVFile, setIsProcessingCSVFile] = useState(false)
|
||||||
|
const [games, setGames] = useState<Game[]>([])
|
||||||
const [page, setPage] = useState(1)
|
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
|
// Reset the page to 1 whenever searchTerm changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@ -562,8 +627,9 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isProcessingCSVFile && <LoadingSpinner desc='Processing games file' />}
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMList IBMSMListFeaturedAlt'>
|
<div className='IBMSMList'>
|
||||||
{filteredGames
|
{filteredGames
|
||||||
.slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE)
|
.slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE)
|
||||||
.map((game) => (
|
.map((game) => (
|
||||||
|
@ -9,13 +9,11 @@ import { ProfilePage } from '../pages/profile'
|
|||||||
import { SettingsPage } from '../pages/settings'
|
import { SettingsPage } from '../pages/settings'
|
||||||
import { SubmitModPage } from '../pages/submitMod'
|
import { SubmitModPage } from '../pages/submitMod'
|
||||||
import { WritePage } from '../pages/write'
|
import { WritePage } from '../pages/write'
|
||||||
import { GamePage } from 'pages/game'
|
|
||||||
|
|
||||||
export const appRoutes = {
|
export const appRoutes = {
|
||||||
index: '/',
|
index: '/',
|
||||||
home: '/home',
|
home: '/home',
|
||||||
games: '/games',
|
games: '/games',
|
||||||
game: '/game/:name',
|
|
||||||
mods: '/mods',
|
mods: '/mods',
|
||||||
mod: '/mod/:naddr',
|
mod: '/mod/:naddr',
|
||||||
about: '/about',
|
about: '/about',
|
||||||
@ -31,9 +29,6 @@ export const appRoutes = {
|
|||||||
profile: '/profile/:nprofile'
|
profile: '/profile/:nprofile'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGamePageRoute = (name: string) =>
|
|
||||||
appRoutes.game.replace(':name', name)
|
|
||||||
|
|
||||||
export const getModPageRoute = (eventId: string) =>
|
export const getModPageRoute = (eventId: string) =>
|
||||||
appRoutes.mod.replace(':naddr', eventId)
|
appRoutes.mod.replace(':naddr', eventId)
|
||||||
|
|
||||||
@ -56,10 +51,6 @@ export const routes = [
|
|||||||
path: appRoutes.games,
|
path: appRoutes.games,
|
||||||
element: <GamesPage />
|
element: <GamesPage />
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: appRoutes.game,
|
|
||||||
element: <GamePage />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: appRoutes.mods,
|
path: appRoutes.mods,
|
||||||
element: <ModsPage />
|
element: <ModsPage />
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
border: solid 1px rgba(255, 255, 255, 0);
|
border: solid 1px rgba(255, 255, 255, 0);
|
||||||
color: rgba(255, 255, 255, 0.1);
|
color: rgba(255, 255, 255, 0.1);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.PaginationMainInsideBox.PaginationMainInsideBoxArrows {
|
.PaginationMainInsideBox.PaginationMainInsideBoxArrows {
|
||||||
@ -94,15 +93,8 @@
|
|||||||
.PaginationMainInsideBoxGroup {
|
.PaginationMainInsideBoxGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: start;
|
justify-content: center;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
overflow: auto;
|
|
||||||
max-width: 470px;
|
|
||||||
height: 47px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.PaginationMainInsideBoxGroup::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@ -110,6 +102,5 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
order: 1;
|
order: 1;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
max-width: unset;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
export type Game = {
|
|
||||||
'Game Name': string
|
|
||||||
'16 by 9 image': string
|
|
||||||
'Boxart image': string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ModFormState {
|
export interface ModFormState {
|
||||||
dTag: string
|
dTag: string
|
||||||
aTag: string
|
aTag: string
|
||||||
|
Loading…
Reference in New Issue
Block a user