From 876f986ea50f8de689a8a7c59810c29f4e0a7a3f Mon Sep 17 00:00:00 2001 From: freakoverse Date: Wed, 18 Sep 2024 13:53:55 +0000 Subject: [PATCH 01/11] added an extra class to the games search results --- src/pages/search.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index ad1ada3..dbff61d 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -629,7 +629,7 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => { <> {isProcessingCSVFile && }
-
+
{filteredGames .slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE) .map((game) => ( -- 2.34.1 From 381028614ae43ed78c2c19a9dc688531a1cc86a6 Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 21:33:22 +0500 Subject: [PATCH 02/11] feat: added a custom hook for games list --- public/assets/games/Games_Itch.csv | 3 + public/assets/games/Games_Other.csv | 4 + .../{games.csv => games/Games_Steam.csv} | 0 src/components/ModForm.tsx | 51 +++------- src/constants.ts | 93 +++++++++++++++++-- src/hooks/index.ts | 2 + src/hooks/useGames.ts | 81 ++++++++++++++++ src/types/mod.ts | 6 ++ 8 files changed, 196 insertions(+), 44 deletions(-) create mode 100644 public/assets/games/Games_Itch.csv create mode 100644 public/assets/games/Games_Other.csv rename public/assets/{games.csv => games/Games_Steam.csv} (100%) create mode 100644 src/hooks/useGames.ts 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 -- 2.34.1 From c62c1a29b922babb54602fc951b82485057de58d Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 21:40:34 +0500 Subject: [PATCH 03/11] chore(refactor): use custom hooks --- src/hooks/useMuteLists.ts | 37 ++++++++++++++++ src/pages/mods.tsx | 28 +++--------- src/pages/search.tsx | 91 ++++----------------------------------- 3 files changed, 52 insertions(+), 104 deletions(-) create mode 100644 src/hooks/useMuteLists.ts diff --git a/src/hooks/useMuteLists.ts b/src/hooks/useMuteLists.ts new file mode 100644 index 0000000..558bcb7 --- /dev/null +++ b/src/hooks/useMuteLists.ts @@ -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 +} diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index e6a5326..7f9982c 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -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([]) 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) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index dbff61d..feac607 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -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, @@ -50,6 +53,7 @@ interface FilterOptions { } export const SearchPage = () => { + const muteLists = useMuteLists() const searchTermRef = useRef(null) const [filterOptions, setFilterOptions] = useState({ sort: SortByEnum.Latest, @@ -57,30 +61,6 @@ export const SearchPage = () => { 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) - }) - }) const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref @@ -278,8 +258,6 @@ const Filters = React.memo( } ) -const MAX_MODS_PER_PAGE = 10 - type ModsResultProps = { filterOptions: FilterOptions searchTerm: string @@ -544,64 +522,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([]) + 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(csvText, { - worker: true, - header: true, - complete: (results) => { - const uniqueGames: Game[] = [] - const gameNames = new Set() - - // 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,7 +555,6 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => { return ( <> - {isProcessingCSVFile && }
{filteredGames -- 2.34.1 From 05414013ce7ed5749868a7306f622ba897d0c477 Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 21:41:55 +0500 Subject: [PATCH 04/11] feat: implemented logic for games page --- src/components/Pagination.tsx | 98 +++++++++++++++++++++++++++++++++++ src/pages/games.tsx | 93 +++++++++++++-------------------- 2 files changed, 134 insertions(+), 57 deletions(-) diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index 5e6e878..591ad3c 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -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 ( +
handlePageChange(page)} // Navigate to the selected page + > +

{page}

+
+ ) + } else { + // For ellipses, render non-clickable dots + return ( +

+ ... +

+ ) + } + }) + } + + return ( +
+
+
+
handlePageChange(currentPage - 1)} + > + +
+
+ {renderPagination()} +
+
handlePageChange(currentPage + 1)} + > + +
+
+
+
+ ) +} diff --git a/src/pages/games.tsx b/src/pages/games.tsx index d2ed7bc..4174648 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -1,9 +1,29 @@ -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 { useState } from 'react' import { GameCard } from '../components/GameCard' +import '../styles/pagination.css' +import '../styles/search.css' +import '../styles/styles.css' export const GamesPage = () => { + 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) + } + } + return (
@@ -11,7 +31,7 @@ export const GamesPage = () => {
-

Games (WIP)

+

Games

@@ -35,61 +55,20 @@ export const GamesPage = () => {
- - - - - -
-
-
-
- + {currentGames.map((game) => ( + + ))}
+
-- 2.34.1 From 72cbd325b0b4e16846253071035c769b9b4771bb Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 21:42:50 +0500 Subject: [PATCH 05/11] feat: add a game page route that will display all mods associated with that game --- src/pages/game.tsx | 299 +++++++++++++++++++++++++++++++++++++++++++ src/routes/index.tsx | 9 ++ 2 files changed, 308 insertions(+) create mode 100644 src/pages/game.tsx diff --git a/src/pages/game.tsx b/src/pages/game.tsx new file mode 100644 index 0000000..3ab9d14 --- /dev/null +++ b/src/pages/game.tsx @@ -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({ + sort: SortByEnum.Latest, + moderated: ModeratedFilterEnum.Moderated + }) + const [mods, setMods] = useState([]) + + 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 && ( + + )} +
+
+
+
+
+
+

+ Game:  + + {gameName} + +

+
+
+
+ +
+
+ {currentMods.map((mod) => { + const route = getModPageRoute( + nip19.naddrEncode({ + identifier: mod.aTag, + pubkey: mod.author, + kind: kinds.ClassifiedListing + }) + ) + + return ( + + ) + })} +
+
+ +
+
+
+ + ) +} + +type FiltersProps = { + filterOptions: FilterOptions + setFilterOptions: Dispatch> +} + +const Filters = React.memo( + ({ filterOptions, setFilterOptions }: FiltersProps) => { + const userState = useAppSelector((state) => state.user) + + return ( +
+
+
+
+ + +
+ {Object.values(SortByEnum).map((item, index) => ( +
+ setFilterOptions((prev) => ({ + ...prev, + sort: item + })) + } + > + {item} +
+ ))} +
+
+
+
+
+ +
+ {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 ( +
+ setFilterOptions((prev) => ({ + ...prev, + moderated: item + })) + } + > + {item} +
+ ) + })} +
+
+
+
+
+ ) + } +) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 3cf4f53..668194b 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -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: }, + { + path: appRoutes.game, + element: + }, { path: appRoutes.mods, element: -- 2.34.1 From a1dd002d284fd5fc0ab24cde3b7bd037a119a408 Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 21:43:36 +0500 Subject: [PATCH 06/11] feat: enabled routing on game card --- src/components/GameCard.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/GameCard.tsx b/src/components/GameCard.tsx index ade5f15..1b34432 100644 --- a/src/components/GameCard.tsx +++ b/src/components/GameCard.tsx @@ -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 ( - +
navigate(getGamePageRoute(title))} + > ) } -- 2.34.1 From 1d02bf0d6f08b5977031f9ec2fe16c66605fcf82 Mon Sep 17 00:00:00 2001 From: daniyal Date: Wed, 18 Sep 2024 22:40:41 +0500 Subject: [PATCH 07/11] feat: navigate to search page on submitting search term in games page --- src/pages/games.tsx | 38 +++++++++++++++++++++++++++++++++++--- src/pages/search.tsx | 11 +++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/pages/games.tsx b/src/pages/games.tsx index 4174648..05dce3a 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -1,13 +1,17 @@ import { PaginationWithPageNumbers } from 'components/Pagination' import { MAX_GAMES_PER_PAGE } from 'constants.ts' import { useGames } from 'hooks' -import { useState } from 'react' +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(null) const games = useGames() const [currentPage, setCurrentPage] = useState(1) @@ -24,6 +28,24 @@ export const GamesPage = () => { } } + 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) => { + if (event.key === 'Enter') { + handleSearch() + } + } + return (
@@ -36,8 +58,18 @@ export const GamesPage = () => {
- -