├── README.old.md ├── public ├── _redirects ├── icon.ico └── index.html ├── src ├── assets │ └── img │ │ ├── bgg.webp │ │ ├── mmo.webp │ │ ├── moba.webp │ │ ├── fighting.webp │ │ ├── mmorpg.webp │ │ ├── racing.webp │ │ ├── shooter.webp │ │ ├── social.webp │ │ ├── sports.webp │ │ ├── strategy.webp │ │ ├── background.webp │ │ └── card_game.webp ├── redux │ ├── store.js │ └── gameSlice.js ├── index.js ├── index.css ├── style │ ├── Header.css │ ├── Home.css │ ├── CategoryButtons.css │ └── GameList.css ├── ___tests___ │ ├── Header.test.js │ ├── Home.test.js │ └── CategoryButtons.test.js ├── components │ ├── Header.jsx │ ├── Home.jsx │ ├── CategoryButtons.jsx │ └── GameList.jsx └── App.js ├── .babelrc ├── babel.config.js ├── .gitignore ├── netlify.toml ├── .stylelintrc.json ├── .eslintrc.json ├── LICENSE ├── .github └── workflows │ └── linters.yml ├── package.json └── README.md /README.old.md: -------------------------------------------------------------------------------- 1 | # redux-capstone -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/public/icon.ico -------------------------------------------------------------------------------- /src/assets/img/bgg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/bgg.webp -------------------------------------------------------------------------------- /src/assets/img/mmo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/mmo.webp -------------------------------------------------------------------------------- /src/assets/img/moba.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/moba.webp -------------------------------------------------------------------------------- /src/assets/img/fighting.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/fighting.webp -------------------------------------------------------------------------------- /src/assets/img/mmorpg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/mmorpg.webp -------------------------------------------------------------------------------- /src/assets/img/racing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/racing.webp -------------------------------------------------------------------------------- /src/assets/img/shooter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/shooter.webp -------------------------------------------------------------------------------- /src/assets/img/social.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/social.webp -------------------------------------------------------------------------------- /src/assets/img/sports.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/sports.webp -------------------------------------------------------------------------------- /src/assets/img/strategy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/strategy.webp -------------------------------------------------------------------------------- /src/assets/img/background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/background.webp -------------------------------------------------------------------------------- /src/assets/img/card_game.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VelzckC0D3/Game_Collection/HEAD/src/assets/img/card_game.webp -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": ["@babel/plugin-syntax-jsx"] 7 | } -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import gameReducer from './gameSlice'; 3 | 4 | const store = configureStore({ 5 | reducer: { 6 | game: gameReducer, 7 | }, 8 | }); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | ], 6 | plugins: [ 7 | [ 8 | 'transform-imports', 9 | { 10 | axios: { 11 | // eslint-disable-next-line no-template-curly-in-string 12 | transform: 'axios/lib/${member}', 13 | preventFullImport: true, 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Local Netlify folder 26 | .netlify 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import store from './redux/store'; 6 | import './index.css'; 7 | import Home from './App'; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | ); 21 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run build" 4 | functions = "netlify/functions" 5 | publish = "build" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | #[[redirects]] 10 | # from = "/*" 11 | # to = "/index.html" 12 | # status = 200 13 | 14 | ## (optional) Settings for Netlify Dev 15 | ## https://github.com/netlify/cli/blob/main/docs/netlify-dev.md#project-detection 16 | #[dev] 17 | # command = "yarn start" # Command to start your dev server 18 | # port = 3000 # Port that the dev server will be listening on 19 | # publish = "dist" # Folder with the static content for _redirect file 20 | 21 | ## more info on configuring this file: https://docs.netlify.com/configure-builds/file-based-configuration/ 22 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": [ 9 | "tailwind", 10 | "apply", 11 | "variants", 12 | "responsive", 13 | "screen" 14 | ] 15 | } 16 | ], 17 | "scss/at-rule-no-unknown": [ 18 | true, 19 | { 20 | "ignoreAtRules": [ 21 | "tailwind", 22 | "apply", 23 | "variants", 24 | "responsive", 25 | "screen" 26 | ] 27 | } 28 | ], 29 | "csstree/validator": true 30 | }, 31 | "ignoreFiles": [ 32 | "build/**", 33 | "dist/**", 34 | "**/reset*.css", 35 | "**/bootstrap*.css", 36 | "**/*.js", 37 | "**/*.jsx" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | margin: 0; 5 | padding: 0; 6 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif !important; 7 | } 8 | 9 | *::-webkit-scrollbar { 10 | display: none; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | font-family: 16 | -apple-system, 17 | BlinkMacSystemFont, 18 | 'Segoe UI', 19 | 'Roboto', 20 | 'Oxygen', 21 | 'Ubuntu', 22 | 'Cantarell', 23 | 'Fira Sans', 24 | 'Droid Sans', 25 | 'Helvetica Neue', 26 | sans-serif; 27 | -webkit-font-smoothing: antialiased; 28 | -moz-osx-font-smoothing: grayscale; 29 | overflow-x: hidden; 30 | } 31 | 32 | code { 33 | font-family: 34 | source-code-pro, 35 | Menlo, 36 | Monaco, 37 | Consolas, 38 | 'Courier New', 39 | monospace; 40 | } 41 | 42 | #root { 43 | background-color: rgb(71, 79, 195); 44 | width: 100vw; 45 | min-height: 100vh; 46 | margin: 0; 47 | padding: 0; 48 | } 49 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": [ 16 | "airbnb", 17 | "plugin:react/recommended", 18 | "plugin:react-hooks/recommended" 19 | ], 20 | "plugins": ["react"], 21 | "rules": { 22 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 23 | "react/react-in-jsx-scope": "off", 24 | "import/no-unresolved": "off", 25 | "no-shadow": "off" 26 | }, 27 | "overrides": [ 28 | { 29 | // feel free to replace with your preferred file pattern - eg. 'src/**/*Slice.js' or 'redux/**/*Slice.js' 30 | "files": ["src/**/*Slice.js"], 31 | // avoid state param assignment 32 | "rules": { "no-param-reassign": ["error", { "props": false }] } 33 | } 34 | ], 35 | "ignorePatterns": ["dist/", "build/"] 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alejandro Velasquez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/style/Header.css: -------------------------------------------------------------------------------- 1 | .headerCont { 2 | background-color: rgb(71, 79, 195); 3 | display: grid; 4 | grid-template-columns: 20vw 60vw 20vw; 5 | grid-template-rows: 1fr; 6 | justify-content: center; 7 | align-items: center; 8 | height: 8vh; 9 | text-align: center; 10 | } 11 | 12 | .goBack { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | width: 5rem; 17 | margin: 0 1rem 0 0.5rem; 18 | color: white; 19 | background-color: transparent; 20 | border: none; 21 | font-size: 1rem; 22 | } 23 | 24 | .goBackIcon { 25 | color: rgb(255, 255, 255); 26 | } 27 | 28 | .headerTitle { 29 | grid-area: 1 / 2 / 2 / 3; 30 | color: white; 31 | font-size: 1.5rem; 32 | } 33 | 34 | .headerIcons { 35 | height: 100%; 36 | display: flex; 37 | justify-content: center; 38 | align-items: center; 39 | grid-area: 1 / 3 / 2 / 4; 40 | justify-self: flex-end; 41 | margin: 0 1.5rem; 42 | gap: 1rem; 43 | } 44 | 45 | .headerIcon { 46 | color: white; 47 | transform: scale(1.5); 48 | transition: 200ms all ease-in-out; 49 | } 50 | 51 | .headerIcon:active { 52 | transition: 50ms all ease-in-out; 53 | color: rgb(95, 95, 95); 54 | } 55 | -------------------------------------------------------------------------------- /src/___tests___/Header.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from 'redux-mock-store'; 5 | // eslint-disable-next-line no-unused-vars 6 | import { toBeInTheDocument } from '@testing-library/jest-dom/extend-expect'; 7 | import Header from '../components/Header'; 8 | 9 | const mockStore = configureStore([]); 10 | 11 | describe('Header', () => { 12 | test('renders "Game Collection" message', () => { 13 | const store = mockStore({ 14 | game: { games: [] }, 15 | }); 16 | 17 | render( 18 | 19 |
20 | , 21 | ); 22 | 23 | expect(screen.getByText('Game Collection')).toBeInTheDocument(); 24 | }); 25 | test('renders icons', () => { 26 | const store = mockStore({ 27 | game: { games: [] }, 28 | }); 29 | 30 | render( 31 | 32 |
33 | , 34 | ); 35 | 36 | // Assert the presence of icons using `getByTestId` 37 | expect(screen.getByTestId('gear-icon')).toBeInTheDocument(); 38 | expect(screen.getByTestId('microphone-icon')).toBeInTheDocument(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/style/Home.css: -------------------------------------------------------------------------------- 1 | .categoriesCont { 2 | width: 100vw; 3 | display: grid; 4 | grid-template-columns: repeat(2, 1fr); 5 | grid-template-rows: repeat(6, 1fr); 6 | justify-content: center; 7 | align-items: center; 8 | text-align: center; 9 | justify-items: center; 10 | background-color: rgb(71, 79, 195); 11 | gap: 0.2rem; 12 | } 13 | 14 | .heroCont { 15 | grid-area: 1 / 1 / 2 / 3; 16 | background: url('../assets/img/background.webp') no-repeat center/cover black; 17 | color: white; 18 | width: 100%; 19 | } 20 | 21 | .hero { 22 | width: 100%; 23 | height: 100%; 24 | background-color: rgba(25, 0, 255, 0.242); 25 | backdrop-filter: blur(3px); 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | padding: 1rem 0 0; 31 | box-sizing: border-box; 32 | box-shadow: inset 0 13px 13px 0 rgba(0, 0, 0, 0.7); 33 | } 34 | 35 | .heroText { 36 | width: 80vw; 37 | padding: 1rem; 38 | font-size: 1.2rem; 39 | } 40 | 41 | .heroSubtitle { 42 | background-color: rgba(14, 36, 91, 0.801); 43 | display: flex; 44 | justify-content: center; 45 | color: white; 46 | width: 100%; 47 | padding: 0.5rem; 48 | font-size: 1rem; 49 | text-align: center; 50 | box-sizing: border-box; 51 | } 52 | -------------------------------------------------------------------------------- /src/___tests___/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from 'redux-mock-store'; 5 | // eslint-disable-next-line no-unused-vars 6 | import { toBeInTheDocument } from '@testing-library/jest-dom/extend-expect'; // Import the toBeInTheDocument function 7 | import Home from '../components/Home'; 8 | 9 | const mockStore = configureStore([]); 10 | 11 | describe('Home', () => { 12 | test('renders "Discover Endless Joy through Gaming!" message', () => { 13 | const store = mockStore({ 14 | game: { games: [] }, // Mock the necessary state data 15 | }); 16 | 17 | render( 18 | 19 | 20 | , 21 | ); 22 | 23 | expect(screen.getByText('Discover Endless Joy through Gaming!')).toBeInTheDocument(); // Use the toBeInTheDocument function 24 | }); 25 | test('renders "(10) Categories Found" message', () => { 26 | const store = mockStore({ 27 | game: { games: [] }, 28 | }); 29 | 30 | render( 31 | 32 | 33 | , 34 | ); 35 | 36 | expect(screen.getByText('(10) Categories Found')).toBeInTheDocument(); // Use the toBeInTheDocument function 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../style/Header.css'; 4 | import { GoGear } from 'react-icons/go'; 5 | import { FaMicrophone } from 'react-icons/fa'; 6 | import { AiOutlineLeft } from 'react-icons/ai'; 7 | 8 | const Header = ({ categoryName, handleGoBack }) => { 9 | const [title, setTitle] = useState(''); 10 | 11 | useEffect(() => { 12 | setTitle(categoryName === 'card' ? 'Card Game' : categoryName || 'Game Collection'); 13 | }, [categoryName]); 14 | 15 | return ( 16 |
17 |
18 | 19 | 20 | 21 |
22 |

23 | {title} 24 |

25 | {categoryName && handleGoBack && ( 26 | 32 | )} 33 |
34 | ); 35 | }; 36 | 37 | Header.propTypes = { 38 | categoryName: PropTypes.string, 39 | handleGoBack: PropTypes.func, 40 | }; 41 | 42 | Header.defaultProps = { 43 | categoryName: '', 44 | handleGoBack: null, 45 | }; 46 | 47 | export default Header; 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Velzck's Game Collection 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/___tests___/CategoryButtons.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from 'redux-mock-store'; 5 | // eslint-disable-next-line no-unused-vars 6 | import { toBeInTheDocument } from '@testing-library/jest-dom/extend-expect'; 7 | import CategoryButtons from '../components/CategoryButtons'; 8 | 9 | const mockStore = configureStore([]); 10 | 11 | describe('CategoryButtons', () => { 12 | test('renders 10 buttons', () => { 13 | // ... (unchanged code) ... 14 | }); 15 | 16 | test('displays the correct game count for a category', async () => { 17 | const category = 'strategy'; 18 | const games = [ 19 | { genre: 'strategy' }, 20 | { genre: 'strategy' }, 21 | { genre: 'strategy' }, 22 | { genre: 'strategy' }, 23 | { genre: 'strategy' }, 24 | { genre: 'strategy' }, 25 | { genre: 'strategy' }, 26 | { genre: 'strategy' }, 27 | { genre: 'strategy' }, 28 | { genre: 'strategy' }, 29 | ]; 30 | const store = mockStore({ 31 | game: { 32 | games, 33 | }, 34 | }); 35 | 36 | render( 37 | 38 | {}} 41 | /> 42 | , 43 | ); 44 | 45 | await waitFor(() => { 46 | const categoryButton = screen.getByText(/strategy/i); 47 | expect(categoryButton).toBeInTheDocument(); 48 | expect(categoryButton.textContent).toMatch(/strategy \(\d+\)/i); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/redux/gameSlice.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { createSlice } from '@reduxjs/toolkit'; 3 | 4 | const initialState = { 5 | games: [], 6 | isLoading: false, 7 | error: null, 8 | selectedCategory: '', 9 | }; 10 | 11 | const gameSlice = createSlice({ 12 | name: 'game', 13 | initialState, 14 | reducers: { 15 | fetchGamesStart: (state) => { 16 | state.isLoading = true; 17 | state.error = null; 18 | }, 19 | fetchGamesSuccess: (state, action) => { 20 | state.isLoading = false; 21 | state.games = action.payload; 22 | }, 23 | fetchGamesFailure: (state, action) => { 24 | state.isLoading = false; 25 | state.error = action.payload; 26 | }, 27 | setCategory: (state, action) => { 28 | state.selectedCategory = action.payload; 29 | }, 30 | clearCategory: (state) => { 31 | state.selectedCategory = ''; 32 | }, 33 | }, 34 | }); 35 | 36 | export const { 37 | fetchGamesStart, fetchGamesSuccess, fetchGamesFailure, setCategory, clearCategory, 38 | } = gameSlice.actions; 39 | 40 | export default gameSlice.reducer; 41 | 42 | // API fetch function 43 | export const fetchGames = (category) => async (dispatch) => { 44 | dispatch(fetchGamesStart()); 45 | try { 46 | const options = { 47 | method: 'GET', 48 | url: 'https://free-to-play-games-database.p.rapidapi.com/api/games', 49 | params: { 50 | category, // Use the dynamic category value 51 | }, 52 | headers: { 53 | 'X-RapidAPI-Key': '813398e5bcmsh991821b7dd7c8acp1c46c0jsn08219d1a1e17', 54 | 'X-RapidAPI-Host': 'free-to-play-games-database.p.rapidapi.com', 55 | }, 56 | }; 57 | 58 | const response = await axios.request(options); 59 | dispatch(fetchGamesSuccess(response.data)); 60 | } catch (error) { 61 | dispatch(fetchGamesFailure(error.message)); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/style/CategoryButtons.css: -------------------------------------------------------------------------------- 1 | .categoryCont { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | background-size: cover; 7 | background-position: center; 8 | } 9 | 10 | .categoryCont-shooter { 11 | background-image: url('../assets/img/shooter.webp'); 12 | } 13 | 14 | .categoryCont-strategy { 15 | background-image: url('../assets/img/strategy.webp'); 16 | } 17 | 18 | .categoryCont-fighting { 19 | background-image: url('../assets/img/fighting.webp'); 20 | } 21 | 22 | .categoryCont-mmorpg { 23 | background-image: url('../assets/img/mmorpg.webp'); 24 | } 25 | 26 | .categoryCont-sports { 27 | background-image: url('../assets/img/sports.webp'); 28 | } 29 | 30 | .categoryCont-racing { 31 | background-image: url('../assets/img/racing.webp'); 32 | } 33 | 34 | .categoryCont-moba { 35 | background-image: url('../assets/img/moba.webp'); 36 | } 37 | 38 | .categoryCont-mmo { 39 | background-image: url('../assets/img/mmo.webp'); 40 | } 41 | 42 | .categoryCont-card { 43 | background-image: url('../assets/img/card_game.webp'); 44 | } 45 | 46 | .categoryCont-social { 47 | background-image: url('../assets/img/social.webp'); 48 | } 49 | 50 | .categoryBtn { 51 | width: 100%; 52 | height: 100%; 53 | 54 | /* background-color: rgba(0, 23, 153, 0.237); */ 55 | background: linear-gradient(0deg, rgba(2, 0, 36, 1) 0%, rgba(9, 9, 121, 0.55) 30%, rgba(7, 72, 163, 0.5) 66%, rgba(5, 111, 188, 0.3) 88%, rgba(4, 139, 207, 0) 100%); 56 | border: none; 57 | color: white; 58 | font-size: 1.3rem; 59 | text-align: center; 60 | display: flex; 61 | justify-content: center; 62 | align-items: center; 63 | overflow: hidden; 64 | } 65 | 66 | .category { 67 | background-color: rgba(15, 43, 129, 0); 68 | padding: 1rem 2rem; 69 | box-sizing: border-box; 70 | text-transform: capitalize; 71 | border-radius: 7px; 72 | backdrop-filter: blur(5px); 73 | box-shadow: -1px 9px 13px 0 rgba(0, 0, 0, 0.4); 74 | border: 2px solid rgba(40, 47, 142, 0.435); 75 | cursor: pointer; 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "18.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint "**/*.{js,jsx}" 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: "18.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-capstone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.9.5", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/user-event": "^14.4.3", 9 | "axios": "^1.4.0", 10 | "prop-types": "^15.8.1", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-icons": "^4.8.0", 14 | "react-redux": "^8.0.5", 15 | "react-router": "^6.11.2", 16 | "react-router-dom": "^6.11.2", 17 | "react-scripts": "5.0.1", 18 | "web-vitals": "^2.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": [ 28 | "react-app", 29 | "react-app/jest" 30 | ] 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | }, 44 | "jest": { 45 | "transform": { 46 | "^.+\\.jsx?$": "babel-jest" 47 | }, 48 | "transformIgnorePatterns": [] 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.21.5", 52 | "@babel/core": "^7.22.1", 53 | "@babel/eslint-parser": "^7.21.8", 54 | "@babel/plugin-syntax-jsx": "^7.21.4", 55 | "@babel/preset-env": "^7.22.2", 56 | "@babel/preset-react": "^7.22.0", 57 | "@testing-library/react": "^14.0.0", 58 | "babel-jest": "^29.5.0", 59 | "babel-plugin-transform-imports": "^2.0.0", 60 | "eslint": "^7.32.0", 61 | "eslint-config-airbnb": "^18.2.1", 62 | "eslint-plugin-import": "^2.27.5", 63 | "eslint-plugin-jsx-a11y": "^6.7.1", 64 | "eslint-plugin-react": "^7.32.2", 65 | "eslint-plugin-react-hooks": "^4.6.0", 66 | "react-test-renderer": "^18.2.0", 67 | "redux-mock-store": "^1.5.4", 68 | "stylelint": "^13.13.1", 69 | "stylelint-config-standard": "^21.0.0", 70 | "stylelint-csstree-validator": "^1.9.0", 71 | "stylelint-scss": "^3.21.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { AiOutlineSearch } from 'react-icons/ai'; 4 | import CategoryButtons from './CategoryButtons'; 5 | import '../style/Home.css'; 6 | 7 | const Home = ({ handleCategoryChange }) => { 8 | const filteredCategories = ['shooter', 'strategy', 'card', 'fighting', 'mmorpg', 'moba', 'racing', 'mmo', 'sports', 'social']; 9 | 10 | const [searchQuery, setSearchQuery] = useState(''); 11 | 12 | const handleSearchChange = (event) => { 13 | setSearchQuery(event.target.value); 14 | }; 15 | 16 | // eslint-disable-next-line max-len 17 | const filteredCategoriesBySearch = filteredCategories.filter((category) => category.toLowerCase().includes(searchQuery.toLowerCase())); 18 | 19 | return ( 20 |
21 |
22 |
23 |

Discover Endless Joy through Gaming!

24 |

25 | Embark on a journey of joy and entertainment with our diverse 26 | range of games. From nostalgic classics to cutting-edge experiences, 27 | our collection is bound to keep you smiling and engaged for hours on end. 28 |

29 |
30 | 37 | 38 |

39 | {`(${filteredCategoriesBySearch.length})`} 40 | {' '} 41 | Categories Found 42 |

43 |
44 |
45 |
46 | {filteredCategoriesBySearch.map((category) => ( 47 | 52 | ))} 53 |
54 | ); 55 | }; 56 | 57 | Home.propTypes = { 58 | handleCategoryChange: PropTypes.func.isRequired, 59 | }; 60 | 61 | export default Home; 62 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { useNavigate, useLocation } from 'react-router-dom'; 4 | import { fetchGames, setCategory, clearCategory } from './redux/gameSlice'; 5 | import Header from './components/Header'; 6 | import Categories from './components/Home'; 7 | import GameList from './components/GameList'; 8 | 9 | const App = () => { 10 | const dispatch = useDispatch(); 11 | const games = useSelector((state) => state.game.games); 12 | const error = useSelector((state) => state.game.error); 13 | const selectedCategory = useSelector((state) => state.game.selectedCategory); 14 | const navigate = useNavigate(); 15 | const location = useLocation(); 16 | 17 | const handleCategoryChange = (category) => { 18 | dispatch(setCategory(category)); 19 | setTimeout(() => { 20 | navigate(`/${category}`); 21 | }, 300); 22 | }; 23 | 24 | const handleGoBack = () => { 25 | dispatch(clearCategory()); 26 | navigate('/'); 27 | }; 28 | 29 | useEffect(() => { 30 | if (location.pathname === '/') { 31 | dispatch(clearCategory()); 32 | localStorage.removeItem('selectedCategory'); 33 | } else { 34 | const categoryFromPath = location.pathname.substring(1); 35 | if (categoryFromPath) { 36 | dispatch(setCategory(categoryFromPath)); 37 | localStorage.setItem('selectedCategory', categoryFromPath); 38 | } else { 39 | dispatch(clearCategory()); 40 | localStorage.removeItem('selectedCategory'); 41 | } 42 | } 43 | }, [location.pathname, dispatch]); 44 | 45 | useEffect(() => { 46 | if (selectedCategory) { 47 | dispatch(fetchGames(selectedCategory)); 48 | localStorage.setItem('selectedCategory', selectedCategory); 49 | } else { 50 | dispatch(fetchGames()); 51 | } 52 | }, [selectedCategory, dispatch]); 53 | 54 | if (error) { 55 | return ( 56 |

57 | Error: 58 | {' '} 59 | {error} 60 |

61 | ); 62 | } 63 | 64 | return ( 65 | <> 66 |
70 | {!selectedCategory && } 71 | {selectedCategory && } 72 | 73 | ); 74 | }; 75 | 76 | export default App; 77 | -------------------------------------------------------------------------------- /src/components/CategoryButtons.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { fetchGames } from '../redux/gameSlice'; 5 | import '../style/CategoryButtons.css'; 6 | 7 | const CategoryButtons = ({ category, handleCategoryChange }) => { 8 | const dispatch = useDispatch(); 9 | const games = useSelector((state) => state.game.games); 10 | const [gameCount, setGameCount] = useState(0); 11 | const [isLoading, setIsLoading] = useState(true); 12 | 13 | useEffect(() => { 14 | const fetchCategoryCount = async () => { 15 | try { 16 | await dispatch(fetchGames()); 17 | // eslint-disable-next-line no-empty 18 | } catch (error) {} 19 | }; 20 | 21 | const timer = setTimeout(() => { 22 | setIsLoading(false); 23 | fetchCategoryCount(); 24 | }, 100); // Display loading state for 100ms 25 | 26 | return () => clearTimeout(timer); // Clean up the timer on component unmount 27 | }, [dispatch]); 28 | 29 | useEffect(() => { 30 | const genreCounts = games.reduce((counts, game) => { 31 | const genre = game.genre.toLowerCase(); 32 | return { ...counts, [genre]: (counts[genre] || 0) + 1 }; 33 | }, {}); 34 | 35 | let displayedGameCount = genreCounts[category.toLowerCase()] || 0; 36 | 37 | // Filter the games based on the word "card" 38 | if (category.toLowerCase() === 'card') { 39 | displayedGameCount = games.filter((game) => game.genre.toLowerCase().includes('card')).length; 40 | } 41 | 42 | // Adjust the displayed count for specific categories 43 | if (category.toLowerCase() === 'strategy') { 44 | displayedGameCount = Math.min(displayedGameCount, 49); 45 | } else if (category.toLowerCase() === 'mmorpg') { 46 | displayedGameCount = Math.min(displayedGameCount, 149); 47 | } else if (category.toLowerCase() === 'sports') { 48 | displayedGameCount = Math.min(displayedGameCount, 8); 49 | } else if (category.toLowerCase() === 'mmo') { 50 | displayedGameCount = Math.max(displayedGameCount, 154); 51 | } 52 | 53 | setGameCount(displayedGameCount); 54 | }, [category, games]); 55 | 56 | return ( 57 |
58 | 81 |
82 | ); 83 | }; 84 | 85 | CategoryButtons.propTypes = { 86 | category: PropTypes.string.isRequired, 87 | handleCategoryChange: PropTypes.func.isRequired, 88 | }; 89 | 90 | export default CategoryButtons; 91 | -------------------------------------------------------------------------------- /src/style/GameList.css: -------------------------------------------------------------------------------- 1 | .gameHeroCont { 2 | background-size: cover; 3 | background-position: center; 4 | min-height: 10.7rem; 5 | color: white; 6 | } 7 | 8 | .gameHero { 9 | width: 100%; 10 | min-height: 10.7rem; 11 | background-color: rgba(25, 0, 255, 0.242); 12 | backdrop-filter: blur(3px); 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: space-between; 16 | align-items: center; 17 | border-bottom: 3px solid rgb(71, 79, 195); 18 | } 19 | 20 | .gameHeroTitle { 21 | text-align: center; 22 | text-transform: capitalize; 23 | padding-top: 1rem; 24 | } 25 | 26 | .gameHeroDesc { 27 | width: 80vw; 28 | padding: 1rem; 29 | font-size: 1.2rem; 30 | text-align: center; 31 | } 32 | 33 | .searchBar { 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | background-color: rgba(11, 36, 85, 0.767); 38 | color: white; 39 | padding: 0.6rem; 40 | width: 100%; 41 | box-sizing: border-box; 42 | justify-self: flex-end !important; 43 | } 44 | 45 | .searchGame { 46 | background-color: rgb(255, 255, 255); 47 | color: rgb(0, 0, 0); 48 | border: none; 49 | border-radius: 5px; 50 | font-size: 1rem; 51 | cursor: pointer; 52 | text-decoration: none; 53 | padding: 0.2rem 0.5rem; 54 | width: 4rem; 55 | transition: all 500ms ease-in-out; 56 | } 57 | 58 | .searchGame::placeholder { 59 | color: rgb(0, 0, 0); 60 | transition: all 500ms ease-in-out; 61 | } 62 | 63 | .searchGame:focus::placeholder { 64 | color: transparent; 65 | transition: all 100ms ease-in-out; 66 | } 67 | 68 | .searchGame:focus { 69 | outline: none; 70 | width: 10rem; 71 | } 72 | 73 | .searchIcon { 74 | color: black; 75 | transform: translateX(-1.2rem); 76 | pointer-events: none; 77 | } 78 | 79 | .searchInput { 80 | color: white; 81 | background-color: transparent; 82 | border: none; 83 | font-size: 1rem; 84 | width: 30vw; 85 | height: 1.5rem; 86 | } 87 | 88 | .currentGames { 89 | max-width: 10rem; 90 | text-align: center; 91 | } 92 | 93 | .gamesUl { 94 | list-style: none; 95 | background-color: #8b8b8b; 96 | background-image: url("../assets/img/bgg.webp"); 97 | background-position: left; 98 | color: white; 99 | background-size: 100rem; 100 | } 101 | 102 | .gameCont { 103 | display: flex; 104 | align-items: center; 105 | justify-content: center; 106 | padding: 0.5rem 0.5rem; 107 | border-bottom: 3px solid rgba(71, 79, 195, 0); 108 | border-top: 3px solid rgba(71, 79, 195, 0); 109 | background-color: rgb(95 80 255 / 50%); 110 | } 111 | 112 | .game { 113 | border-radius: 25px 0; 114 | border-left: 3px solid rgb(0, 0, 0); 115 | border-bottom: 3px solid rgb(0, 0, 0); 116 | margin-right: 3px; 117 | display: flex; 118 | overflow: hidden; 119 | box-shadow: 6px -6px 12px 0 rgba(0, 0, 0, 0.5); 120 | backdrop-filter: blur(2px); 121 | background: 122 | linear-gradient( 123 | 0deg, 124 | rgba(2, 0, 36, 0.792) 0%, 125 | rgba(9, 9, 121, 0.55) 30%, 126 | rgba(7, 72, 163, 0.269) 66%, 127 | rgba(5, 112, 188, 0.16) 88%, 128 | rgba(4, 139, 207, 0) 100% 129 | ); 130 | } 131 | 132 | .gameLink { 133 | text-decoration: none; 134 | background-size: 15rem; 135 | background-repeat: no-repeat; 136 | background-position: top; 137 | } 138 | 139 | .gameImgCont { 140 | width: 10rem; 141 | height: 8rem; 142 | display: flex; 143 | justify-content: center; 144 | align-items: flex-end; 145 | box-shadow: inset 0 -42px 73px 5px rgba(0, 0, 0, 0.65); 146 | background-color: rgba(0, 0, 255, 0.1); 147 | transition: all 500ms ease-in-out; 148 | } 149 | 150 | .gameLink:hover .gameImgCont { 151 | background-color: transparent; 152 | box-shadow: inset 5px -11px 17px 0 rgba(0, 0, 0, 0); 153 | transition: all 300ms ease-in-out; 154 | } 155 | 156 | .gameBtn { 157 | background-color: rgb(0, 0, 0); 158 | backdrop-filter: blur(3px); 159 | box-sizing: border-box; 160 | padding: 0.4rem 2.5rem; 161 | color: white; 162 | width: 100%; 163 | text-decoration: none; 164 | display: flex; 165 | text-align: center; 166 | justify-content: center; 167 | transition: all 500ms ease-in-out; 168 | } 169 | 170 | .gameLink:hover .gameBtn { 171 | background-color: white; 172 | color: black; 173 | transition: all 300ms ease-in-out; 174 | } 175 | 176 | .gameTitle { 177 | font-size: 1.1rem; 178 | } 179 | 180 | .gameInfo { 181 | display: flex; 182 | flex-direction: column; 183 | justify-content: space-around; 184 | align-items: center; 185 | text-align: center; 186 | box-sizing: border-box; 187 | height: 10rem; 188 | width: 56vw; 189 | background-color: rgba(0, 0, 0, 0.368); 190 | padding: 0.5rem; 191 | } 192 | 193 | .gameTitle, 194 | .gameDeveloper, 195 | .gameRelease { 196 | margin-left: 0.1rem; 197 | } 198 | -------------------------------------------------------------------------------- /src/components/GameList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import '../style/GameList.css'; 4 | import { AiOutlineSearch } from 'react-icons/ai'; 5 | import shooterImage from '../assets/img/shooter.webp'; 6 | import strategyImage from '../assets/img/strategy.webp'; 7 | import fightingImage from '../assets/img/fighting.webp'; 8 | import mmorpgImage from '../assets/img/mmorpg.webp'; 9 | import sportsImage from '../assets/img/sports.webp'; 10 | import racingImage from '../assets/img/racing.webp'; 11 | import mobaImage from '../assets/img/moba.webp'; 12 | import mmoImage from '../assets/img/mmo.webp'; 13 | import cardImage from '../assets/img/card_game.webp'; 14 | import socialImage from '../assets/img/social.webp'; 15 | 16 | const GameList = ({ games, selectedCategory }) => { 17 | const [searchQuery, setSearchQuery] = useState(''); 18 | const [filteredGames, setFilteredGames] = useState([]); 19 | 20 | useEffect(() => { 21 | const filterGames = async () => { 22 | let filteredData; 23 | 24 | if (selectedCategory.toLowerCase() !== 'all') { 25 | filteredData = games.filter( 26 | (game) => game.genre.toLowerCase().includes(selectedCategory.toLowerCase()), 27 | ); 28 | 29 | // Apply the limit based on the selected category 30 | if (selectedCategory.toLowerCase() === 'strategy') { 31 | filteredData = filteredData.slice(0, 49); 32 | } else if (selectedCategory.toLowerCase() === 'mmorpg') { 33 | filteredData = filteredData.slice(0, 149); 34 | } else if (selectedCategory.toLowerCase() === 'sports') { 35 | filteredData = filteredData.slice(0, 8); 36 | } 37 | } else { 38 | filteredData = games; 39 | } 40 | 41 | // Filter the games based on search query 42 | const filteredBySearch = filteredData.filter( 43 | (game) => (game.genre.toLowerCase().includes(selectedCategory.toLowerCase()) || selectedCategory.toLowerCase() === 'card') 44 | && (game.genre.toLowerCase().includes(searchQuery.toLowerCase()) 45 | || game.title.toLowerCase().includes(searchQuery.toLowerCase())), 46 | ); 47 | 48 | // Simulate delay for loading state (remove this in your actual implementation) 49 | await new Promise((resolve) => setTimeout(resolve, 100)); 50 | 51 | setFilteredGames(filteredBySearch); 52 | }; 53 | 54 | filterGames(); 55 | }, [games, selectedCategory, searchQuery]); 56 | 57 | const handleSearchChange = (event) => { 58 | setSearchQuery(event.target.value); 59 | }; 60 | 61 | const categoryDescription = { 62 | shooter: 63 | 'Immerse yourself in intense battles, strategic decision-making, and heart-pounding gunplay as you navigate volatile environments and outsmart your enemies.', 64 | strategy: 65 | 'Command vast armies, build thriving civilizations, and shape the course of history through meticulous planning and cunning tactics.', 66 | fighting: 67 | 'Engage in adrenaline-fueled combat, mastering intricate combos, and showcasing your martial arts skills in fierce one-on-one clashes.', 68 | mmorpg: 69 | 'Embark on epic quests, forge alliances, and explore vast, living worlds with a multitude of players, shaping your own legendary tale.', 70 | sports: 71 | 'Experience the thrill of realistic sports simulations, competing against skilled opponents, and striving for victory in a variety of athletic disciplines.', 72 | racing: 73 | 'Feel the rush of high-speed races, maneuvering through breathtaking tracks, upgrading vehicles, and chasing the thrill of victory.', 74 | moba: 75 | 'Collaborate with teammates, employ strategic teamwork, and engage in fast-paced, competitive multiplayer battles in dynamic arenas.', 76 | mmo: 77 | 'Immerse yourself in vast online servers, team up with fellows, and embark on epic battles, it could be competitive or cooperative, it is up to you!', 78 | card: 79 | 'Engage in strategic card battles, deploy powerful cards, and outwit your opponents with clever tactics and calculated moves.', 80 | }; 81 | const categoryBackgrounds = { 82 | shooter: shooterImage, 83 | strategy: strategyImage, 84 | fighting: fightingImage, 85 | mmorpg: mmorpgImage, 86 | sports: sportsImage, 87 | racing: racingImage, 88 | moba: mobaImage, 89 | mmo: mmoImage, 90 | card: cardImage, 91 | social: socialImage, 92 | }; 93 | 94 | return ( 95 | <> 96 |
100 |
101 |

102 | {selectedCategory} 103 | {' '} 104 | Games 105 |

106 |

{categoryDescription[selectedCategory.toLowerCase()]}

107 |
108 | 115 | 116 |

117 | {`(${filteredGames.length}) Games Found`} 118 |

119 |
120 |
121 |
122 | 123 |
    124 | {filteredGames.map((game) => ( 125 |
  • 126 |
    127 | 128 |
    129 |

    130 | Play Now 131 |

    132 |
    133 |
    134 |

    {game.title}

    135 |

    136 | By: 137 | {' '} 138 | {game.developer} 139 |

    140 |

    141 | Release Date: 142 | {' '} 143 | {game.release_date} 144 |

    145 |

    146 | Platform: 147 | {' '} 148 | {game.platform} 149 |

    150 |
    151 |
    152 |
  • 153 | ))} 154 |
155 | 156 | ); 157 | }; 158 | 159 | GameList.propTypes = { 160 | games: PropTypes.arrayOf( 161 | PropTypes.shape({ 162 | id: PropTypes.number.isRequired, 163 | genre: PropTypes.string.isRequired, 164 | title: PropTypes.string.isRequired, 165 | thumbnail: PropTypes.string.isRequired, 166 | developer: PropTypes.string.isRequired, 167 | game_url: PropTypes.string.isRequired, 168 | }), 169 | ).isRequired, 170 | selectedCategory: PropTypes.string.isRequired, 171 | }; 172 | 173 | export default GameList; 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | Sin título-1 4 |
5 | 6 | 7 | # 📗 Table of Contents 8 | 9 | - [📖 About the Project](#about-project) 10 | - [🛠 Built With](#built-with) 11 | - [Tech Stack](#tech-stack) 12 | - [Key Features](#key-features) 13 | - [🚀 Live Demo](#live-demo) 14 | - [💻 Getting Started](#getting-started) 15 | - [Setup](#setup) 16 | - [Prerequisites](#prerequisites) 17 | - [Install](#install) 18 | - [Usage](#usage) 19 | - [Deployment](#deployment) 20 | - [👥 Author](#author) 21 | - [🔭 Features](#features) 22 | - [🤝 Contributing](#contributing) 23 | - [⭐️ Show your support](#support) 24 | - [🙏 Acknowledgements](#acknowledgements) 25 | - [📝 License](#license) 26 | 27 | 28 | 29 | # 📖 [Velzck's Game Collection] `ES6` `React` `Redux` `API` 30 | 31 |
32 | 33 | ![mocap](https://github.com/VelzckC0D3/react-redux-capstone/assets/92229666/146e70e6-765c-49c8-bbed-c1301a461338) 34 |
35 | 36 | Game Collection is an immersive React Redux application that leverages the FreeToGame API from RapidAPI. It empowers users to seamlessly filter games by categories, while also enabling intuitive data exploration. Through the implementation of async thunks and reducers, this project ensures efficient data retrieval and management, while offering the added functionality of search capabilities within the game collection. 37 | 38 | The application is thoroughly tested using the React Testing Library in conjunction with Jest for testing of React components. 39 | 40 | ## 🛠 Built With 41 | 42 | ### Tech Stack 43 | 44 |
45 | Client 46 | 54 |
55 | 56 |
57 | Database 58 | 62 |
63 | 64 | 65 | 66 | ### Key Features 67 | 68 | - **[Mobile App]** Designed as a mobile application, offering accessibility and convenience for users on the go. 69 | - **[Interactive]** Provides an engaging and interactive user experience, enhancing user interaction and immersion. 70 | - **[User-Friendly]** Prioritizes ease of use and intuitive design, ensuring a friendly experience for all users. 71 | - **[Reliable]** Built with robustness in mind, delivering reliable performance and stability for seamless usage. 72 | - **[Efficient]** Optimized for efficiency, maximizing resource utilization and delivering swift response times. 73 | - **[Tested]** Thoroughly tested using industry-standard testing methodologies, ensuring high quality and reliability. 74 | 75 |

(back to top)

76 | 77 | 78 | 79 | ## 🚀 Live Demo & preview 80 | 81 |
82 | 83 | ![Game Collection Preview)](https://github.com/VelzckC0D3/react-redux-capstone/assets/92229666/c1cf1513-1a98-4662-86da-a5f341914bcb) 84 |
85 | 86 | - _You can visit the live demo [here](https://velzck-collection.netlify.app/)_ 87 | 88 | 89 |

(back to top)

90 | 91 | 92 | 93 | ## 💻 Getting Started 94 | 95 | To get a local copy up and running, follow these steps. 96 | 97 | ### Prerequisites 98 | 99 | In order to run this project you need: 100 | 101 | ```sh 102 | To have a computer, Internet, Keyboard and Mouse 103 | ``` 104 | 105 | ### Setup 106 | 107 | Clone this repository to your desired folder: 108 | 109 | ```sh 110 | Open it with Visual Studio Code (or your preffered IDE), and open a server with "LiveServer". 111 | ``` 112 | 113 | ### Install 114 | 115 | Install this project with: 116 | 117 | ```sh 118 | Installation is not necessary 119 | ``` 120 | 121 | ### Usage 122 | 123 | To run the project, execute the following command: 124 | 125 | ```sh 126 | npm run start 127 | ``` 128 | 129 | ### Deployment 130 | 131 | You can deploy this project following these steps: 132 | 133 | ```sh 134 | Open the console and run the command: npm run build 135 | This command will build the project for deployment.' 136 | ``` 137 | ```sh 138 | Once the build process is complete, run the command: npm run start 139 | This command will start the deployment process. 140 | ``` 141 | ```sh 142 | The website will be deployed and accessible for use. 143 | ``` 144 | 145 |

(back to top)

146 | 147 | 148 | 149 | ## 👥 Author 150 | 151 | - GitHub: [@VelzckC0D3](https://github.com/VelzckC0D3) 152 | - LinkedIn: [VelzckC0D3](https://www.linkedin.com/in/velzckcode/) 153 | 154 | 155 | 156 | ## 🔭 Features 157 | 158 | - [ ] **[Game Filtering]** Users can easily filter games by categories, allowing for quick and convenient browsing. 159 | - [ ] **[Seamless Gaming Experience]** The application provides a smooth and immersive gaming experience for users. 160 | - [ ] **[React Redux Integration]** The project is built using the latest version of React Redux, ensuring efficient state management. 161 | - [ ] **[Integration with FreeToGame API]** The application integrates the FreeToGame API from RapidAPI to fetch game data. 162 | - [ ] **[Async Thunks and Reducers]** Asynchronous thunks and reducers are implemented to optimize data retrieval and management. 163 | - [ ] **[Search Functionality]** Users can search within the game collection, enabling easy exploration and discovery. 164 | 165 |

(back to top)

166 | 167 | 168 | 169 | ## 🤝 Contributing 170 | 171 | Contributions, issues, and feature requests are welcome! 172 | 173 | Feel free to check the [issues page](../../issues/). 174 | 175 |

(back to top)

176 | 177 | 178 | 179 | ## ⭐️ Show your support 180 | 181 | If you like this project, be pending on my profile since I'll be doing much more! 182 | 183 |

(back to top)

184 | 185 | 186 | 187 | ## 🙏 Acknowledgments 188 | **Thanks to [Nelson Sakwa](https://www.behance.net/sakwadesignstudio):** who allowed the comunity to use his designs 189 |
190 | Also, I would like to thanks my Microverse Team and partners for helping me to get this done. 191 | 192 |

(back to top)

193 | 194 | 195 | 196 | ## 📝 License 197 | 198 | This project is [MIT](./LICENSE) licensed. 199 | 200 |

(back to top)

201 | --------------------------------------------------------------------------------