Multi-file games lists, clicking a game leads to mod search for that game, search redirects, pagination optimizations #38

Merged
freakoverse merged 11 commits from staging into master 2024-09-18 19:30:26 +00:00
17 changed files with 753 additions and 213 deletions

View File

@ -0,0 +1,3 @@
Game Name,16 by 9 image,Boxart image
Voices of the Void,,https://image.nostr.build/472949882d0756c84d3effd9f641b10c88abd48265f0f01f360937b189d50b54.jpg
Shroom and Gloom,,
1 Game Name 16 by 9 image Boxart image
2 Voices of the Void https://image.nostr.build/472949882d0756c84d3effd9f641b10c88abd48265f0f01f360937b189d50b54.jpg
3 Shroom and Gloom

View File

@ -0,0 +1,4 @@
Game Name,16 by 9 image,Boxart image
Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
Vintage Story,,
Yandere Simulator,,
1 Game Name 16 by 9 image Boxart image
2 Minecraft https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
3 Vintage Story
4 Yandere Simulator

View File

Can't render this file because it is too large.

View File

@ -1,5 +1,7 @@
import { useNavigate } from 'react-router-dom'
import '../styles/cardGames.css'
import { handleGameImageError } from '../utils'
import { getGamePageRoute } from 'routes'
type GameCardProps = {
title: string
@ -7,8 +9,13 @@ type GameCardProps = {
}
export const GameCard = ({ title, imageUrl }: GameCardProps) => {
const navigate = useNavigate()
return (
<a className='cardGameMainWrapperLink' href='search.html'>
<div
className='cardGameMainWrapperLink'
onClick={() => navigate(getGamePageRoute(title))}
>
<div className='cardGameMainWrapper'>
<img
src={imageUrl}
@ -19,6 +26,6 @@ export const GameCard = ({ title, imageUrl }: GameCardProps) => {
<div className='cardGameMainTitle'>
<p>{title}</p>
</div>
</a>
</div>
)
}

View File

@ -1,6 +1,5 @@
import _ from 'lodash'
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import Papa from 'papaparse'
import React, {
Fragment,
useCallback,
@ -9,11 +8,16 @@ import React, {
useRef,
useState
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { FixedSizeList as List } from 'react-window'
import { v4 as uuidv4 } from 'uuid'
import { useAppSelector } from '../hooks'
import { T_TAG_VALUE } from '../constants'
import { RelayController } from '../controllers'
import { useAppSelector, useGames } from '../hooks'
import { appRoutes, getModPageRoute } from '../routes'
import '../styles/styles.css'
import { DownloadUrl, ModDetails, ModFormState } from '../types'
import {
initializeFormState,
isReachable,
@ -24,12 +28,7 @@ import {
now
} from '../utils'
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 { T_TAG_VALUE } from '../constants'
interface FormErrors {
game?: string
@ -48,8 +47,6 @@ interface GameOption {
label: string
}
let processedCSV = false
type ModFormProps = {
existingModData?: ModDetails
}
@ -57,6 +54,7 @@ type ModFormProps = {
export const ModForm = ({ existingModData }: ModFormProps) => {
const location = useLocation()
const navigate = useNavigate()
const games = useGames()
const userState = useAppSelector((state) => state.user)
const [isPublishing, setIsPublishing] = useState(false)
@ -64,6 +62,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
const [formState, setFormState] = useState<ModFormState>(
initializeFormState(existingModData)
)
const [formErrors, setFormErrors] = useState<FormErrors>({})
useEffect(() => {
if (location.pathname === appRoutes.submitMod) {
@ -71,35 +70,13 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
}
}, [location.pathname]) // Only trigger when the pathname changes to submit-mod
const [formErrors, setFormErrors] = useState<FormErrors>({})
useEffect(() => {
if (processedCSV) return
processedCSV = 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 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)
}
})
})
.catch((error) => console.error('Error fetching CSV file:', error))
}, [])
const options = games.map((game) => ({
label: game['Game Name'],
value: game['Game Name']
}))
setGameOptions(options)
}, [games])
const handleInputChange = useCallback((name: string, value: string) => {
setFormState((prevState) => ({

View File

@ -38,3 +38,101 @@ 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>
)
}

View File

@ -43,14 +43,93 @@ export const LANDING_PAGE_DATA = {
// Both of these arrays can have separate items
export const REACTIONS = {
positive: {
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:']
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:'
]
},
negative: {
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:']
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:'
]
}
}
// 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

View File

@ -1,3 +1,5 @@
export * from './redux'
export * from './useDidMount'
export * from './useGames'
export * from './useMuteLists'
export * from './useReactions'

81
src/hooks/useGames.ts Normal file
View File

@ -0,0 +1,81 @@
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
}

37
src/hooks/useMuteLists.ts Normal file
View File

@ -0,0 +1,37 @@
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
}

299
src/pages/game.tsx Normal file
View File

@ -0,0 +1,299 @@
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:&nbsp;
<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>
)
}
)

View File

@ -1,9 +1,51 @@
import '../styles/pagination.css'
import '../styles/styles.css'
import '../styles/search.css'
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/search.css'
import '../styles/styles.css'
import { createSearchParams, useNavigate } from 'react-router-dom'
import { appRoutes } from 'routes'
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 (
<div className='InnerBodyMain'>
<div className='ContainerMain'>
@ -11,13 +53,23 @@ export const GamesPage = () => {
<div className='IBMSecMain'>
<div className='SearchMainWrapper'>
<div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Games (WIP)</h2>
<h2 className='IBMSMTitleMainHeading'>Games</h2>
</div>
<div className='SearchMain'>
<div className='SearchMainInside'>
<div className='SearchMainInsideWrapper'>
<input type='text' className='SMIWInput' />
<button className='btn btnMain SMIWButton' type='button'>
<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'
@ -35,61 +87,20 @@ export const GamesPage = () => {
</div>
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList IBMSMListFeaturedAlt'>
<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'
/>
<GameCard
title='This is a game title, the best game title'
imageUrl='/assets/img/DEGMods%20Placeholder%20Img.png'
/>
</div>
</div>
<div className='IBMSecMain'>
<div className='PaginationMain'>
<div className='PaginationMainInside'>
<a
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
href='#'
>
<i className='fas fa-chevron-left'></i>
</a>
<div className='PaginationMainInsideBoxGroup'>
<a className='PaginationMainInsideBox PMIBActive' href='#'>
<p>1</p>{' '}
</a>
<a className='PaginationMainInsideBox' href='#'>
<p>2</p>{' '}
</a>
<a className='PaginationMainInsideBox' href='#'>
<p>3</p>
</a>
<p className='PaginationMainInsideBox PMIBDots'>...</p>
<a className='PaginationMainInsideBox' href='#'>
<p>8</p>
</a>
</div>
<a
className='PaginationMainInsideBox PaginationMainInsideBoxArrows'
href='#'
>
<i className='fas fa-chevron-right'></i>
</a>
</div>
{currentGames.map((game) => (
<GameCard
key={game['Game Name']}
title={game['Game Name']}
imageUrl={game['Boxart image']}
/>
))}
</div>
</div>
<PaginationWithPageNumbers
currentPage={currentPage}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { Pagination } from 'components/Pagination'
import { kinds, nip19 } from 'nostr-tools'
import React, {
Dispatch,
@ -9,17 +10,16 @@ import React, {
} from 'react'
import { LoadingSpinner } from '../components/LoadingSpinner'
import { ModCard } from '../components/ModCard'
import { MOD_FILTER_LIMIT } from '../constants'
import { MetadataController } from '../controllers'
import { useAppSelector, useDidMount } from '../hooks'
import { useAppSelector, useDidMount, useMuteLists } from '../hooks'
import { getModPageRoute } from '../routes'
import '../styles/filters.css'
import '../styles/pagination.css'
import '../styles/search.css'
import '../styles/styles.css'
import { ModDetails, MuteLists } from '../types'
import { ModDetails } from '../types'
import { fetchMods } from '../utils'
import { MOD_FILTER_LIMIT } from '../constants'
import { Pagination } from 'components/Pagination'
enum SortBy {
Latest = 'Latest',
@ -56,19 +56,8 @@ export const ModsPage = () => {
source: window.location.host,
moderated: ModeratedFilter.Moderated
})
const [muteLists, setMuteLists] = useState<{
admin: MuteLists
user: MuteLists
}>({
admin: {
authors: [],
replaceableEvents: []
},
user: {
authors: [],
replaceableEvents: []
}
})
const muteLists = useMuteLists()
const [nsfwList, setNSFWList] = useState<string[]>([])
const [page, setPage] = useState(1)
@ -76,12 +65,7 @@ export const ModsPage = () => {
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)
})
metadataController.getNSFWList().then((list) => {
setNSFWList(list)

View File

@ -5,12 +5,15 @@ 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 {
MAX_GAMES_PER_PAGE,
MAX_MODS_PER_PAGE,
T_TAG_VALUE
} from 'constants.ts'
import { RelayController } from 'controllers'
import { useAppSelector, useGames, useMuteLists } from 'hooks'
import { Filter, kinds, nip19 } from 'nostr-tools'
import { Subscription } from 'nostr-tools/abstract-relay'
import Papa from 'papaparse'
import React, {
Dispatch,
SetStateAction,
@ -19,6 +22,7 @@ import React, {
useRef,
useState
} from 'react'
import { useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import { getModPageRoute } from 'routes'
import { ModDetails, MuteLists } from 'types'
@ -50,37 +54,20 @@ interface FilterOptions {
}
export const SearchPage = () => {
const [searchParams] = useSearchParams()
const muteLists = useMuteLists()
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)
})
searching:
(searchParams.get('searching') as SearchingFilterEnum) ||
SearchingFilterEnum.Mods
})
const [searchTerm, setSearchTerm] = useState(
searchParams.get('searchTerm') || ''
)
const handleSearch = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref
@ -278,8 +265,6 @@ const Filters = React.memo(
}
)
const MAX_MODS_PER_PAGE = 10
type ModsResultProps = {
filterOptions: FilterOptions
searchTerm: string
@ -544,64 +529,14 @@ const UsersResult = ({
)
}
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 games = useGames()
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)
@ -627,9 +562,8 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
return (
<>
{isProcessingCSVFile && <LoadingSpinner desc='Processing games file' />}
<div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'>
<div className='IBMSMList IBMSMListFeaturedAlt'>
{filteredGames
.slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE)
.map((game) => (

View File

@ -9,11 +9,13 @@ import { ProfilePage } from '../pages/profile'
import { SettingsPage } from '../pages/settings'
import { SubmitModPage } from '../pages/submitMod'
import { WritePage } from '../pages/write'
import { GamePage } from 'pages/game'
export const appRoutes = {
index: '/',
home: '/home',
games: '/games',
game: '/game/:name',
mods: '/mods',
mod: '/mod/:naddr',
about: '/about',
@ -29,6 +31,9 @@ export const appRoutes = {
profile: '/profile/:nprofile'
}
export const getGamePageRoute = (name: string) =>
appRoutes.game.replace(':name', name)
export const getModPageRoute = (eventId: string) =>
appRoutes.mod.replace(':naddr', eventId)
@ -51,6 +56,10 @@ export const routes = [
path: appRoutes.games,
element: <GamesPage />
},
{
path: appRoutes.game,
element: <GamePage />
},
{
path: appRoutes.mods,
element: <ModsPage />

View File

@ -43,6 +43,7 @@
border: solid 1px rgba(255, 255, 255, 0);
color: rgba(255, 255, 255, 0.1);
font-weight: bold;
white-space: nowrap;
}
.PaginationMainInsideBox.PaginationMainInsideBoxArrows {
@ -93,8 +94,15 @@
.PaginationMainInsideBoxGroup {
display: flex;
flex-direction: row;
justify-content: center;
justify-content: start;
grid-gap: 10px;
overflow: auto;
max-width: 470px;
height: 47px;
}
.PaginationMainInsideBoxGroup::-webkit-scrollbar {
display: none;
}
@media (max-width: 768px) {
@ -102,5 +110,6 @@
width: 100%;
order: 1;
justify-content: space-around;
max-width: unset;
}
}

View File

@ -1,3 +1,9 @@
export type Game = {
'Game Name': string
'16 by 9 image': string
'Boxart image': string
}
export interface ModFormState {
dTag: string
aTag: string