Compare commits

..

No commits in common. "d01521a5f02ad00235e441e17c6877bc6fa7149e" and "9cb3d2fb63a26110f9d3d0cf1c40902f7a20fb3a" have entirely different histories.

17 changed files with 212 additions and 752 deletions

View File

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

View File

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

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

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

View File

@ -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) => ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:&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,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>

View File

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

View File

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

View File

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

View File

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

View File

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