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
8 changed files with 196 additions and 44 deletions
Showing only changes of commit 381028614a - Show all commits

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,6 +1,5 @@
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,
@ -9,11 +8,16 @@ 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 { 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 '../styles/styles.css'
import { DownloadUrl, ModDetails, ModFormState } from '../types'
import { import {
initializeFormState, initializeFormState,
isReachable, isReachable,
@ -24,12 +28,7 @@ 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
@ -48,8 +47,6 @@ interface GameOption {
label: string label: string
} }
let processedCSV = false
type ModFormProps = { type ModFormProps = {
existingModData?: ModDetails existingModData?: ModDetails
} }
@ -57,6 +54,7 @@ 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)
@ -64,6 +62,7 @@ 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) {
@ -71,35 +70,13 @@ 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(() => {
if (processedCSV) return const options = games.map((game) => ({
processedCSV = true label: game['Game Name'],
value: game['Game Name']
// Fetch the CSV file from the public folder }))
fetch('/assets/games.csv') setGameOptions(options)
.then((response) => response.text()) }, [games])
.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 handleInputChange = useCallback((name: string, value: string) => { const handleInputChange = useCallback((name: string, value: string) => {
setFormState((prevState) => ({ setFormState((prevState) => ({

View File

@ -43,14 +43,93 @@ 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,3 +1,5 @@
export * from './redux' export * from './redux'
export * from './useDidMount' export * from './useDidMount'
export * from './useGames'
export * from './useMuteLists'
export * from './useReactions' 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
}

View File

@ -1,3 +1,9 @@
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