diff --git a/public/assets/games/Games_Itch.csv b/public/assets/games/Games_Itch.csv new file mode 100644 index 0000000..7734f31 --- /dev/null +++ b/public/assets/games/Games_Itch.csv @@ -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,, \ No newline at end of file diff --git a/public/assets/games/Games_Other.csv b/public/assets/games/Games_Other.csv new file mode 100644 index 0000000..5a83dfb --- /dev/null +++ b/public/assets/games/Games_Other.csv @@ -0,0 +1,4 @@ +Game Name,16 by 9 image,Boxart image +Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg +Vintage Story,, +Yandere Simulator,, \ No newline at end of file diff --git a/public/assets/games.csv b/public/assets/games/Games_Steam.csv similarity index 100% rename from public/assets/games.csv rename to public/assets/games/Games_Steam.csv diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index e95f1dc..982fc75 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -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( initializeFormState(existingModData) ) + const [formErrors, setFormErrors] = useState({}) 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({}) - 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) => ({ diff --git a/src/constants.ts b/src/constants.ts index 16a3eef..14bf6be 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 178d686..09362c9 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,3 +1,5 @@ export * from './redux' export * from './useDidMount' +export * from './useGames' +export * from './useMuteLists' export * from './useReactions' diff --git a/src/hooks/useGames.ts b/src/hooks/useGames.ts new file mode 100644 index 0000000..cbbd51a --- /dev/null +++ b/src/hooks/useGames.ts @@ -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([]) + + useEffect(() => { + if (hasProcessedFiles.current) return + + hasProcessedFiles.current = true + + const readGamesCSVs = async () => { + const uniqueGames: Game[] = [] + const gameNames = new Set() + + // Function to promisify PapaParse + const parseCSV = (csvText: string) => + new Promise((resolve, reject) => { + Papa.parse(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 +} diff --git a/src/types/mod.ts b/src/types/mod.ts index b161187..710f631 100644 --- a/src/types/mod.ts +++ b/src/types/mod.ts @@ -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