Multi-file games lists, clicking a game leads to mod search for that game, search redirects, pagination optimizations #38
3
public/assets/games/Games_Itch.csv
Normal file
3
public/assets/games/Games_Itch.csv
Normal 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,,
|
|
4
public/assets/games/Games_Other.csv
Normal file
4
public/assets/games/Games_Other.csv
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Game Name,16 by 9 image,Boxart image
|
||||||
|
Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
|
||||||
|
Vintage Story,,
|
||||||
|
Yandere Simulator,,
|
|
Can't render this file because it is too large.
|
@ -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) => ({
|
||||||
|
@ -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
|
||||||
|
@ -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
81
src/hooks/useGames.ts
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user