├── 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 |
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 |
{
62 | handleCategoryChange(category);
63 | const topElement = document.getElementById('headerCont');
64 | if (topElement) {
65 | topElement.scrollIntoView();
66 | }
67 | }}
68 | >
69 | {isLoading ? (
70 | Loading...
71 | ) : (
72 |
73 | {category === 'card' ? 'Card Game' : category}
74 | {' '}
75 | (
76 | {gameCount}
77 | )
78 |
79 | )}
80 |
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 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------