diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..c0a45b6
Binary files /dev/null and b/bun.lockb differ
diff --git a/config-overrides.js b/config-overrides.js
new file mode 100644
index 0000000..a1cdbc4
--- /dev/null
+++ b/config-overrides.js
@@ -0,0 +1,9 @@
+module.exports = function override(webpackConfig) {
+ webpackConfig.module.rules.push({
+ test: /\.mjs$/,
+ include: /node_modules/,
+ type: 'javascript/auto',
+ })
+
+ return webpackConfig
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..33ec6ce
--- /dev/null
+++ b/index.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Games Database
+
+
+
+
+
+
+
+
+
+
diff --git a/notes.txt b/notes.txt
new file mode 100644
index 0000000..8627da4
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1,3 @@
+Include somewhere with image on error page. Probably in video alt.
+
+Image by Alexander Antropov from Pixabay
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ea09056
--- /dev/null
+++ b/package.json
@@ -0,0 +1,76 @@
+{
+ "name": "games-database-client",
+ "version": "0.1.0",
+ "private": true,
+ "dependencies": {
+ "@chakra-ui/icons": "^2.1.1",
+ "@chakra-ui/react": "^2.8.2",
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@hookstate/core": "^4.0.1",
+ "@hookstate/localstored": "^4.0.2",
+ "axios": "^1.5.0",
+ "bson-objectid": "^2.0.4",
+ "chakra-react-select": "^4.7.2",
+ "chakra-ui-contextmenu": "^1.0.5",
+ "framer-motion": "^10.16.4",
+ "interweave": "^13.1.0",
+ "jodit-react": "^1.3.39",
+ "js-base64": "^3.7.5",
+ "react": "^18.2.0",
+ "react-device-detect": "^2.2.3",
+ "react-dom": "^18.2.0",
+ "react-hook-form": "^7.48.2",
+ "react-icons": "^4.11.0",
+ "react-rating": "^2.0.5",
+ "react-responsive-carousel": "^3.2.23",
+ "react-router-dom": "^5.3.4",
+ "react-select": "^5.7.4",
+ "react-toastify": "^9.1.3",
+ "web-vitals": "^3.4.0"
+ },
+ "scripts": {
+ "start": "bunx --bun vite",
+ "buildProject": "bunx --bun vite build",
+ "serve": "vite preview",
+ "serveprod": "serve ./dist/index.html"
+ },
+ "eslintConfig": {
+ "extends": [
+ "react-app",
+ "react-app/jest"
+ ]
+ },
+ "resolutions": {
+ "@types/react": "18.2.21",
+ "@types/react-dom": "18.2.7"
+ },
+ "browserslist": {
+ "production": [
+ ">0.2%",
+ "not dead",
+ "not op_mini all"
+ ],
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ]
+ },
+ "devDependencies": {
+ "@chakra-ui/cli": "^2.4.1",
+ "@hookstate/devtools": "^4.0.1",
+ "@testing-library/jest-dom": "^6.1.3",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.4.3",
+ "@types/jest": "^29.5.4",
+ "@types/node": "^20.6.0",
+ "@types/react": "^18.2.21",
+ "@types/react-dom": "^18.2.7",
+ "@types/react-router-dom": "^5.3.3",
+ "@types/react-table": "^7.7.15",
+ "@vitejs/plugin-react": "^4.0.4",
+ "typescript": "^5.2.2",
+ "vite": "^4.4.9"
+ }
+}
diff --git a/public/assets/Logo.svg b/public/assets/Logo.svg
new file mode 100644
index 0000000..3a14d3e
--- /dev/null
+++ b/public/assets/Logo.svg
@@ -0,0 +1,51 @@
+
+
+
diff --git a/public/assets/SteamAndDeckLogo.svg b/public/assets/SteamAndDeckLogo.svg
new file mode 100644
index 0000000..4fd83d3
--- /dev/null
+++ b/public/assets/SteamAndDeckLogo.svg
@@ -0,0 +1,34 @@
+
+
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..f9fadb4
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/manifest.json b/public/manifest.json
new file mode 100644
index 0000000..7d3bb95
--- /dev/null
+++ b/public/manifest.json
@@ -0,0 +1,15 @@
+{
+ "short_name": "Games Database",
+ "name": "Games Database",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..696bf08
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.js.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/src/@types/game.ts b/src/@types/game.ts
new file mode 100644
index 0000000..02d1a97
--- /dev/null
+++ b/src/@types/game.ts
@@ -0,0 +1,3 @@
+export type RecursivePartial = {
+ [P in keyof T]?: RecursivePartial;
+};
\ No newline at end of file
diff --git a/src/App.test.tsx b/src/App.test.tsx
new file mode 100644
index 0000000..2a68616
--- /dev/null
+++ b/src/App.test.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import App from './App';
+
+test('renders learn react link', () => {
+ render();
+ const linkElement = screen.getByText(/learn react/i);
+ expect(linkElement).toBeInTheDocument();
+});
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..fc7db19
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,112 @@
+import React, { useEffect } from 'react'
+import GameDashboard from './components/GamesDashboard'
+import { Redirect, Route, Switch, useLocation } from 'react-router-dom'
+import Header from './components/Header'
+import Home from './components/Home'
+import GameDetails from './components/GameDetails'
+import GameForm from './components/GameForm'
+import NotFound from './errors/NotFound'
+import TestErrors from './errors/TestError'
+import Footer from './components/Footer'
+import LoginForm from './components/authComponents/LoginForm'
+import {
+ setAppLoaded,
+ getUser,
+ appLoadedState,
+ loggedInState, loadedPreferences,
+} from './stateManagement/userState'
+import {useHookstate} from '@hookstate/core'
+import { ToastContainer } from 'react-toastify'
+import NoGames from './components/NoGames'
+import CreateUser from './components/userComponents/CreateUser'
+import LoadingModal from './components/LoadingModal'
+import SomethingWentWrong from './errors/SomethingWentWrong'
+import ForgotPassword from './components/authComponents/ForgotPassword'
+import ResetPassword from './components/authComponents/ResetPassword'
+import UserProfile from './components/userComponents/UserProfile'
+import EditUser from './components/userComponents/EditUser'
+import {Box, useColorMode} from "@chakra-ui/react";
+
+function App() {
+ const location = useLocation()
+ const appLoaded = useHookstate(appLoadedState)
+ const appLoad = appLoaded.get()
+ const isLoggedIn = useHookstate(loggedInState)
+ const loggedIn = isLoggedIn.get()
+ const originalToken = window.localStorage.getItem('jwt')
+ const { colorMode, toggleColorMode } = useColorMode()
+
+ useEffect(() => {
+ if (originalToken) getUser(originalToken).finally(() => setAppLoaded(true))
+ else setAppLoaded(false)
+ }, [originalToken])
+
+ if (!appLoad)
+
+ useEffect(() => {
+ if(loadedPreferences.get().theme !== colorMode) {
+ localStorage.removeItem('chakra-ui-color-mode')
+ toggleColorMode()
+ }
+ }, [loadedPreferences.get().theme]);
+
+ // noinspection SpellCheckingInspection
+ return (
+ <>
+
+ {loggedIn ? : ''}
+
+ {loggedIn ? : }
+
+
+ {/**/}
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loggedIn ? : ''}
+ >
+ )
+}
+
+export default App
\ No newline at end of file
diff --git a/src/api/agent.ts b/src/api/agent.ts
new file mode 100644
index 0000000..921f89b
--- /dev/null
+++ b/src/api/agent.ts
@@ -0,0 +1,128 @@
+import axios, {AxiosError, AxiosResponse} from 'axios'
+import Game, {Data, GameList} from '../models/game'
+import {toast} from 'react-toastify'
+import {history} from '..'
+import User, {IForgotPassword, IResetPassword, UserAPI, UserFormValues, UserUpdateValues} from '../models/user'
+import {userToken} from '../stateManagement/userState'
+import {RecursivePartial} from "../@types/game";
+
+const sleep = (delay: number) => {
+ return new Promise((resolve) => {
+ setTimeout(resolve, delay)
+ })
+}
+
+axios.defaults.baseURL = import.meta.env.VITE_API_URL
+
+axios.interceptors.request.use((config) => {
+ const token = userToken.get()
+ if (token) config.headers.Authorization = `Bearer ${token}`
+ return config
+})
+
+axios.interceptors.response.use(
+ async (response) => {
+ await sleep(0)
+ return response
+ },
+ (error: AxiosError) => {
+ const {data, status, config} = error.response!
+ switch (status) {
+ case 400:
+ // @ts-ignore
+ if (typeof data.error === 'string') toast.error(data.error)
+ if (config.method === 'get') history.push('/not-found')
+ // @ts-ignore
+ if (data.errors) {
+ const modalStateErrors = []
+ // @ts-ignore
+ for (const key in data.errors) {
+ // @ts-ignore
+ if (data.errors[key]) modalStateErrors.push(data.errors[key])
+ }
+ throw modalStateErrors.flat()
+ }
+ break
+ case 401:
+ toast.error('Unauthorized')
+ history.push('/')
+ break
+ case 404:
+ history.push('/not-found')
+ break
+ case 429:
+ history.push('/something-went-wrong')
+ break
+ case 500:
+ toast.error('Server Error')
+ break
+ }
+ return Promise.reject(error)
+ }
+)
+
+const responseBody = (response: AxiosResponse) => response.data
+
+interface Tags {
+ success: boolean,
+ data: {
+ genre: string[]
+ series: string[]
+ store: string[]
+ developer: string[]
+ publisher: string[]
+ }
+}
+
+const requests = {
+ get: (url: string) => axios.get(url).then(responseBody),
+ post: (url: string, body: {}) =>
+ axios.post(url, body).then(responseBody),
+ put: (url: string, body: {}) => axios.put(url, body).then(responseBody),
+ delete: (url: string) => axios.delete(url).then(responseBody),
+}
+
+const Games = {
+ list: (filter: string) => requests.get(`/games?${filter}`),
+ details: (id: string) => requests.get(`/games/${id}`),
+ create: (game: Data) => axios.post('/games', game),
+ update: (id: string, game: Data) => axios.put(`/games/${id}`, game),
+ playStatus: (id: string, playStatus: RecursivePartial) => axios.put(`/games/${id}`, playStatus),
+ rating: (id: string, rating: RecursivePartial) => axios.put(`/games/${id}`, rating),
+ delete: (id: string) => axios.delete(`/games/${id}`),
+}
+
+const GamesAdmin = {
+ list: (filter: string) => requests.get(`/admin/games?${filter}`),
+ details: (id: string) => requests.get(`/admin/games/${id}`),
+ create: (game: Data) => axios.post('/admin/games', game),
+ update: (id: string, game: Data) => axios.put(`/admin/games/${id}`, game),
+ delete: (id: string) => axios.delete(`/admin/games/${id}`),
+}
+
+const RetrieveTags = {
+ tags: () => requests.get('/tags'),
+ genres: () => requests.get('/tags/genres'),
+ series: () => requests.get('/tags/series'),
+ store: () => requests.get('/tags/store'),
+ developer: () => requests.get('/tags/developer'),
+ publisher: () => requests.get('/tags/publisher'),
+}
+
+const Account = {
+ current: () => requests.get('/auth/me'),
+ login: (user: UserFormValues) => requests.post('/auth/login', user),
+ register: (user: UserFormValues) => requests.post('/auth/register', user),
+ forgot: (email: IForgotPassword) => requests.post('/auth/forgotpassword', email),
+ reset: (token: string, password: IResetPassword) => requests.put(`/auth/resetpassword/${token}`, password),
+ update: (user: UserUpdateValues) => requests.put('/auth/updatedetails', user)
+}
+
+const agent = {
+ Games,
+ GamesAdmin,
+ RetrieveTags,
+ Account,
+}
+
+export default agent
diff --git a/src/componentUtils/SelectValues.ts b/src/componentUtils/SelectValues.ts
new file mode 100644
index 0000000..b32031f
--- /dev/null
+++ b/src/componentUtils/SelectValues.ts
@@ -0,0 +1,40 @@
+import stringArrayToSelectObject from './stringArrayToSelectObject'
+
+export const controllerValues = stringArrayToSelectObject(['Full Controller Support', 'Partial Controller Support', 'No Controller Support'])
+
+export const wineIntelValues = stringArrayToSelectObject(['Yes', 'No', 'Not Tested'])
+
+export const playStatusValues = stringArrayToSelectObject(['Never Played', 'Up Next', 'Playing', 'Finished', 'Will Not Play'])
+
+export const ratingValues = stringArrayToSelectObject(["0","0.5","1","1.5","2","2.5","3","3.5","4","4.5","5","5.5","6","6.5","7","7.5","8","8.5","9","9.5","10"])
+
+export const ratingStateValues = [
+ { label: '=', value: '[]' },
+ { label: '<=', value: '[lte]' },
+ { label: '>=', value: '[gte]' },
+]
+
+export const osValues = [
+ {
+ value: 'windows',
+ label: 'Windows'
+ },
+ {
+ value: 'mac',
+ label: 'Mac OSX'
+ },
+ {
+ value: 'linux',
+ label: 'Linux'
+ },
+ {
+ value: 'android',
+ label: 'Android'
+ },
+ {
+ value: 'ios',
+ label: 'iOS'
+ }
+]
+
+export const yesNo = stringArrayToSelectObject(['Yes', 'No'])
\ No newline at end of file
diff --git a/src/componentUtils/changeActiveFilterHeader.ts b/src/componentUtils/changeActiveFilterHeader.ts
new file mode 100644
index 0000000..05cf20c
--- /dev/null
+++ b/src/componentUtils/changeActiveFilterHeader.ts
@@ -0,0 +1,25 @@
+import {State} from "@hookstate/core";
+import {Filters} from "../models/game";
+
+export const ChangeActiveFilterHeader = (
+ filters: Filters,
+ searchParams: string,
+ setShowFiltersModuleState: State
+) => {
+ if (
+ searchParams.length > 0 ||
+ filters.series.length > 0 ||
+ filters.playStatus.length > 0 ||
+ filters.genre.length > 0 ||
+ filters.os.length > 0 ||
+ filters.store.length > 0 ||
+ filters.developer.length > 0 ||
+ filters.publisher.length > 0 ||
+ filters.controller.length > 0 ||
+ filters.soundtrack.length > 0 ||
+ filters.intel.length > 0 ||
+ filters.wine.length > 0 ||
+ filters.rating.length > 0
+ ) setShowFiltersModuleState.set(true)
+ else setShowFiltersModuleState.set(false)
+}
diff --git a/src/componentUtils/getWithFilters.ts b/src/componentUtils/getWithFilters.ts
new file mode 100644
index 0000000..8aee1cb
--- /dev/null
+++ b/src/componentUtils/getWithFilters.ts
@@ -0,0 +1,39 @@
+import {State} from '@hookstate/core'
+import {Filters, GameList} from '../models/game'
+import agent from '../api/agent'
+
+
+const getWithFilters = async (
+ gamesState: State,
+ loadedState: State,
+ admin: boolean,
+ page: number,
+ limit: number,
+ search: string,
+ filters: Filters,
+ ratingState: string,
+ steamRatingState: string,
+) => {
+ loadedState.set(false)
+ const filterString = 'page=' + page.toString() +
+ '&limit=' + limit.toString() +
+ '&search=' + encodeURIComponent(search) +
+ '&series=' + encodeURIComponent(filters.series) +
+ '&playStatus=' + encodeURIComponent(filters.playStatus) +
+ '&genre=' + encodeURIComponent(filters.genre) +
+ '&os=' + encodeURIComponent(filters.os) +
+ '&store=' + encodeURIComponent(filters.store) +
+ '&developer=' + encodeURIComponent(filters.developer) +
+ '&publisher=' + encodeURIComponent(filters.publisher) +
+ '&controller=' + encodeURIComponent(filters.controller) +
+ '&soundtrack=' + encodeURIComponent(filters.soundtrack) +
+ '&intel=' + encodeURIComponent(filters.intel) +
+ '&wine=' + encodeURIComponent(filters.wine) +
+ '&rating' + (filters.rating !== '' ? ratingState + '=' + encodeURIComponent(filters.rating.toString()) : '=') +
+ '&steamRating' + (filters.steamRating !== '' ? steamRatingState + '=' + encodeURIComponent(filters.steamRating.toString()) : '=')
+ const getGames = admin ? await agent.GamesAdmin.list(filterString) : await agent.Games.list(filterString)
+ gamesState.set(getGames)
+ loadedState.set(true)
+}
+
+export default getWithFilters
\ No newline at end of file
diff --git a/src/componentUtils/retrieveTags.ts b/src/componentUtils/retrieveTags.ts
new file mode 100644
index 0000000..f3c65df
--- /dev/null
+++ b/src/componentUtils/retrieveTags.ts
@@ -0,0 +1,18 @@
+import agent from '../api/agent'
+import { tagState } from '../stateManagement/tagState'
+import stringArrayToSelectObject from './stringArrayToSelectObject'
+
+
+export const retrieveTags = async () => {
+ const tagsAPI = await agent.RetrieveTags.tags()
+ const genres = stringArrayToSelectObject(tagsAPI.data.genre)
+ const series = stringArrayToSelectObject(tagsAPI.data.series)
+ const store = stringArrayToSelectObject(tagsAPI.data.store)
+ const developer = stringArrayToSelectObject(tagsAPI.data.developer)
+ const publisher = stringArrayToSelectObject(tagsAPI.data.publisher)
+ tagState.genres.merge(genres)
+ tagState.series.set(series)
+ tagState.store.set(store)
+ tagState.developer.set(developer)
+ tagState.publisher.set(publisher)
+}
diff --git a/src/componentUtils/stringArrayToSelectObject.ts b/src/componentUtils/stringArrayToSelectObject.ts
new file mode 100644
index 0000000..b4be3fa
--- /dev/null
+++ b/src/componentUtils/stringArrayToSelectObject.ts
@@ -0,0 +1,8 @@
+const stringArrayToSelectObject = (arr: string[]) => {
+ return arr.reduce((prev: { value: string, label: string }[], item) => {
+ prev.push({ value: item, label: item })
+ return prev
+ }, [])
+}
+
+export default stringArrayToSelectObject;
\ No newline at end of file
diff --git a/src/components/ActiveFilters.tsx b/src/components/ActiveFilters.tsx
new file mode 100644
index 0000000..0c2720f
--- /dev/null
+++ b/src/components/ActiveFilters.tsx
@@ -0,0 +1,132 @@
+import React from 'react'
+import { useHookstate } from '@hookstate/core'
+import { gameDashboardState } from '../stateManagement/gameState'
+import { Flex, Stack, HStack, Link, Spacer, Tag, TagCloseButton, TagLabel } from '@chakra-ui/react'
+import getWithFilters from '../componentUtils/getWithFilters'
+import {
+ loadingState,
+ openAndCloseSearchDialogue,
+ searchCompletedState,
+ showFiltersModuleState
+} from '../stateManagement/loadingState'
+import { adminMode } from '../stateManagement/userState'
+import ResetFilterButton from './helperComponents/ResetFilterButton'
+import { ChangeActiveFilterHeader } from "../componentUtils/changeActiveFilterHeader";
+import { ratingState, steamRatingState } from "../stateManagement/ratingState";
+
+const ActiveFilters = () => {
+ const setShowFiltersModuleState = useHookstate(showFiltersModuleState)
+ const showFiltersModule = setShowFiltersModuleState.get()
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ const { filters, searchParams } = games
+ const loadedState = useHookstate(loadingState)
+ const admin = useHookstate(adminMode).get()
+ const setSearchModalOpen = useHookstate(openAndCloseSearchDialogue)
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+ const searchComplete = useHookstate(searchCompletedState)
+
+ const removeFilters = async (filterHeader: string, filter: string) => {
+ const newFilter = filters[filterHeader as keyof typeof filters].replace(filter, '')
+ const finalFilter = { ...filters, [filterHeader]: newFilter }
+
+ await ChangeActiveFilterHeader(finalFilter, searchParams, setShowFiltersModuleState)
+
+ return await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ games.count.currentPage,
+ games.count.limit,
+ searchParams,
+ finalFilter,
+ useRatingState,
+ useSteamRatingState,
+ )
+ }
+
+ return showFiltersModule ? (
+
+ {searchParams.length >= 1 && (
+ <>
+ Search:
+
+ setSearchModalOpen.set(true)}>{searchParams}
+ {
+ const newSearchParams = ""
+ ChangeActiveFilterHeader(filters, newSearchParams, setShowFiltersModuleState)
+ searchComplete.set(false)
+ return await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ games.count.currentPage,
+ games.count.limit,
+ newSearchParams,
+ filters,
+ useRatingState,
+ useSteamRatingState,
+ )
+ }} />
+
+ >
+ )}
+
+ Active Filters:
+ {Object.keys(filters).map((filter: string, num: number) => {
+ const filterValues: string = filters[filter as keyof typeof filters]
+ const filterValuesLength: number = filterValues.length
+ return filterValuesLength >= 1 && (
+ <>
+
+ {filter}
+
+ {filters[filter as keyof typeof filters].split(',').map((item: string, index) => (
+
+ {
+ item.includes('lte')
+ ? item.replace('lte', '<= ')
+ : item.includes('gte') ? item.replace('gte', '>= ')
+ : item}
+
+ removeFilters(filter, item)} />
+
+ ))}
+
+
+ >
+ )
+ })}
+
+
+
+
+ ) : <>>
+}
+
+export default ActiveFilters
diff --git a/src/components/DeleteDialogue.tsx b/src/components/DeleteDialogue.tsx
new file mode 100644
index 0000000..e33870a
--- /dev/null
+++ b/src/components/DeleteDialogue.tsx
@@ -0,0 +1,77 @@
+import React, { useRef } from 'react'
+import agent from '../api/agent'
+import { useHistory } from 'react-router-dom'
+import { Data } from '../models/game'
+import {
+ AlertDialog,
+ AlertDialogBody,
+ AlertDialogContent, AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogOverlay,
+ Button, Icon, Text
+} from '@chakra-ui/react'
+
+interface Props {
+ id: string
+ open: boolean
+ setOpen: Function
+ setLoading: Function
+ game: Data
+}
+
+export default function DeleteDialogue({
+ id,
+ open,
+ setOpen,
+ setLoading,
+ game,
+}: Props) {
+ const history = useHistory()
+ const handleClose = () => {
+ setOpen(false)
+ }
+
+ const deleteGame = async (id: string) => {
+ setLoading(true)
+ try {
+ await agent.Games.delete(id)
+ setLoading(false)
+ handleClose()
+ history.push(`/games/`)
+ } catch (err) {
+ console.error(err)
+ setLoading(false)
+ }
+ }
+
+ const cancelRef = useRef(null)
+
+ return (
+
+
+
+
+ Delete?
+
+ Are you sure you would like to delete {game.title}?
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx
new file mode 100644
index 0000000..ea36a8f
--- /dev/null
+++ b/src/components/FilterBar.tsx
@@ -0,0 +1,350 @@
+import React, {MutableRefObject, useEffect} from 'react'
+import {
+ Center,
+ Divider,
+ Drawer,
+ DrawerBody,
+ DrawerCloseButton,
+ DrawerContent,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay, Flex,
+ Heading, Spacer,
+ Text,
+} from '@chakra-ui/react'
+import AdminMode from './authComponents/AdminMode'
+import {StateMethods, useHookstate} from '@hookstate/core'
+import {tagState} from '../stateManagement/tagState'
+import {
+ controllerValues,
+ osValues,
+ playStatusValues, ratingStateValues,
+ ratingValues,
+ wineIntelValues,
+ yesNo
+} from '../componentUtils/SelectValues'
+import {retrieveTags} from '../componentUtils/retrieveTags'
+import {gameDashboardState} from '../stateManagement/gameState'
+import {Select} from 'chakra-react-select'
+import getWithFilters from '../componentUtils/getWithFilters'
+import {loadingState, showFiltersModuleState} from '../stateManagement/loadingState'
+import {adminMode} from '../stateManagement/userState'
+import ResetFilterButton from "./helperComponents/ResetFilterButton";
+import {ChangeActiveFilterHeader} from "../componentUtils/changeActiveFilterHeader";
+import {ratingState, steamRatingState} from "../stateManagement/ratingState";
+
+interface Props {
+ filterIsOpen: boolean
+ filterOnClose: () => void
+ btnRef: MutableRefObject
+ filterLoadingState: StateMethods
+}
+
+function FilterBar({filterIsOpen, filterOnClose, btnRef}: Props) {
+ const gameState = useHookstate(gameDashboardState)
+ const games = gameState.get()
+ const {filters, searchParams} = games
+ const setShowFiltersModuleState = useHookstate(showFiltersModuleState)
+ const loadedState = useHookstate(loadingState)
+ const admin = useHookstate(adminMode).get()
+ const tagsState = useHookstate(tagState)
+ const tags = tagsState.get()
+ const setRatingState = useHookstate(ratingState)
+ const useRatingState = setRatingState.get()
+ const setSteamRatingState = useHookstate(steamRatingState)
+ const useSteamRatingState = setSteamRatingState.get()
+
+ const getTags = () => {
+ if (tags.developer.length < 2) return retrieveTags()
+ else return
+ }
+
+ useEffect(() => {
+ return () => {
+ getTags()
+ }
+ }, [tags.developer])
+
+ const filterToSelectDefaultValue = (filterName: string) => {
+ if (filters[filterName as keyof typeof filters] === "") return
+ return filters[filterName as keyof typeof filters].split(',').map(value => {
+ return {label: value, value: value}
+ })
+ }
+
+ const ratingToSelectDefaultValue = (value: string) => {
+ let label: string = '='
+ if (value === "") return
+ if (value === "[lte]") label = '<='
+ if (value === "[gte]") label = '>='
+ return { label, value }
+ }
+
+ const changeFilters = async (filterName: string, value: string) => {
+ const finalFilter = {...filters, [filterName]: value}
+
+ await ChangeActiveFilterHeader(finalFilter, searchParams, setShowFiltersModuleState)
+
+ return await getWithFilters(
+ gameState,
+ loadedState,
+ admin,
+ games.count.currentPage,
+ games.count.limit,
+ games.searchParams,
+ finalFilter,
+ useRatingState,
+ useSteamRatingState
+ )
+ }
+
+ return (
+
+
+
+
+
+ Filters
+
+
+
+
+
+
+
+
+
+ Series
+
+
+
+
+
+
+
+ )
+}
+
+export default FilterBar
\ No newline at end of file
diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx
new file mode 100644
index 0000000..2091fc0
--- /dev/null
+++ b/src/components/Footer.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+import { Box, Stack } from "@chakra-ui/react";
+
+export default function Footer() {
+ // noinspection SpellCheckingInspection
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/GameDetails.tsx b/src/components/GameDetails.tsx
new file mode 100644
index 0000000..8d1f37b
--- /dev/null
+++ b/src/components/GameDetails.tsx
@@ -0,0 +1,156 @@
+import React, { useEffect } from 'react'
+import agent from '../api/agent'
+import { useParams } from 'react-router'
+import {
+ Box,
+ Button,
+ Container,
+ Flex,
+ Grid,
+ Heading,
+ Link as ChakraLink,
+ Spacer,
+ Stack,
+ Text,
+ useColorModeValue,
+ useDisclosure,
+ useMediaQuery,
+} from '@chakra-ui/react'
+import ImageSlider from './detailsComponents/ImageSlider'
+import Summary from './detailsComponents/Summary'
+import Features from './detailsComponents/Features'
+import SystemRequirements from './detailsComponents/SystemRequirementsMenu'
+import { Link } from 'react-router-dom'
+import { detailedGameState } from '../stateManagement/gameState'
+import { loadingState } from '../stateManagement/loadingState'
+import { ImmutableObject, useHookstate } from '@hookstate/core'
+import LoadingModal from './LoadingModal'
+import SteamAndDeckLogo from './helperComponents/SteamAndDeckLogo'
+import { ExternalLinkIcon } from '@chakra-ui/icons'
+import { Data } from '../models/game'
+import SummaryModal from './detailsComponents/SummaryModal'
+import SystemRequirementsModal from './detailsComponents/SystemRequirementsModal'
+
+const GameDetails = () => {
+ const gameState = useHookstate(detailedGameState)
+ const game: ImmutableObject = gameState.get().data
+ const loadedState = useHookstate(loadingState)
+ const loaded = loadedState.get()
+ const { id } = useParams<{ id: string }>()
+ const bg = useColorModeValue('white', 'gray.700')
+ const { isOpen: isSummaryOpen, onOpen: onSummaryOpen, onClose: onSummaryClose } = useDisclosure()
+ const { isOpen: isSystemOpen, onOpen: onSystemOpen, onClose: onSystemClose } = useDisclosure()
+ const [isSmallerThan768] = useMediaQuery('(max-width: 768px)')
+
+ useEffect(() => {
+ const getGame = async () => {
+ loadedState.set(false)
+ return await agent.Games.details(id)
+ }
+ getGame().then((result) => {
+ gameState.set(result)
+ document.title = result.data.title
+ loadedState.set(true)
+ })
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ if (!loaded) return
+
+ // @ts-ignore
+ // @ts-ignore
+ return (
+ <>
+ {isSummaryOpen ?
+
+
+ : ''}
+ {isSystemOpen ?
+
+
+ : ''}
+
+
+
+ {game.title}
+
+
+
+ {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
+ (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {isSmallerThan768 ?
+
+
+
+
+ :
+ }
+
+
+
+
+
+
+
+ {isSmallerThan768 ?
+
+
+
+
+ :
+ }
+
+
+
+
+ >
+ )
+}
+
+export default GameDetails
diff --git a/src/components/GameForm.tsx b/src/components/GameForm.tsx
new file mode 100644
index 0000000..80eac5e
--- /dev/null
+++ b/src/components/GameForm.tsx
@@ -0,0 +1,597 @@
+import React, {FormEvent, useEffect, useRef} from 'react'
+import {useHistory, useParams} from 'react-router-dom'
+import agent from '../api/agent'
+import ObjectID from 'bson-objectid'
+import {AccessedBy, Data, Os, SystemRequirements, WindowsOrMacOrLinux,} from '../models/game'
+import {controllerValues, playStatusValues, wineIntelValues, yesNo,} from '../componentUtils/SelectValues'
+import DeleteDialogue from './DeleteDialogue'
+import {Options} from 'react-select'
+import stringArrayToSelectObject from '../componentUtils/stringArrayToSelectObject'
+import {
+ loadingAndContinueState,
+ loadingAndSaveState,
+ loadingState,
+ openAndCloseDeleteDialogue,
+} from '../stateManagement/loadingState'
+import {useHookstate} from '@hookstate/core'
+import {tagState} from '../stateManagement/tagState'
+import {retrieveTags} from '../componentUtils/retrieveTags'
+import LoadingModal from './LoadingModal'
+import {
+ Box,
+ Button,
+ Container,
+ FormControl,
+ FormLabel,
+ Heading,
+ InputGroup,
+ Spacer,
+ Stack,
+ Switch,
+ Text,
+ useColorModeValue,
+} from '@chakra-ui/react'
+import {CreatableSelect,} from 'chakra-react-select'
+import {adminMode} from '../stateManagement/userState'
+import {encode} from 'js-base64'
+import SingleFieldInput from './formFields/SingleFieldInput'
+import TextareaInput from './formFields/TextareaInput'
+import SystemRequirementsTextareaInput from './formFields/SystemRequirementsTextareaInput'
+import SelectInput from './formFields/SelectInput'
+import CreatableSelectInput from './formFields/CreatableSelectInput'
+import CheckboxInput from './formFields/CheckboxInput'
+import { changeLoadState } from "../stateManagement/loadingState";
+import SingleFieldNumericInput from './formFields/SingleFieldNumericInput'
+
+const GameForm = () => {
+ const loadState = useHookstate(loadingState)
+ const loaded = loadState.get()
+ const submitLoadingState = useHookstate(loadingAndSaveState)
+ const submitLoading = submitLoadingState.get()
+ const history = useHistory()
+ const openState = useHookstate(openAndCloseDeleteDialogue)
+ const open = openState.get()
+ const moveOnState = useHookstate(loadingAndContinueState)
+ const moveOn = moveOnState.get()
+ const {id} = useParams<{ id: string }>()
+ const tagsState = useHookstate(tagState)
+ const tags = tagsState.get()
+ const bg = useColorModeValue('gray.50', 'transparent')
+ const admin = useHookstate(adminMode).get()
+
+ const gameState = useHookstate({
+ _id: '',
+ title: '',
+ series: '',
+ steamId: '',
+ slug: '',
+ frontImage: '',
+ screenshots: [{id: 0, full: '', thumbnail: ''}],
+ genre: [''],
+ os: {
+ windows: true,
+ mac: false,
+ linux: false,
+ android: false,
+ ios: false
+ },
+ wine: 'Not Tested',
+ controller: 'No Controller Support',
+ developer: [''],
+ publisher: [''],
+ releaseDate: '',
+ shortDesc: '',
+ reviews: '',
+ summary: '',
+ intel: 'Not Tested',
+ steamRating: 0,
+ systemRequirements: {
+ windows: {
+ minimum: '',
+ recommended: '',
+ },
+ mac: {
+ minimum: '',
+ recommended: '',
+ },
+ linux: {
+ minimum: '',
+ recommended: '',
+ },
+ },
+ accessedBy: [{
+ user: '',
+ store: ['Steam'],
+ playStatus: 'Never Played',
+ soundtrack: 'No',
+ rating: 0,
+ }],
+ createDate: '',
+ lastUpdateDate: '',
+ lastModifiedBy: '',
+ scrape: true,
+ })
+ const isMountedRef = useRef(false)
+ const game = gameState.get()
+ const gameId = game._id
+
+ const getGame = async (id: string, admin: boolean) => {
+ changeLoadState(true)
+ const result = admin ? await agent.GamesAdmin.details(id) : await agent.Games.details(id)
+ return result.data
+ }
+
+
+ useEffect(() => {
+ document.title = id ? `Edit ${game.title}` : 'Create Game'
+ })
+
+ const getTags = () => {
+ if (tags.developer.length < 2) return retrieveTags()
+ else return
+ }
+
+ useEffect(() => {
+ isMountedRef.current = true;
+
+ (async () => {
+
+ changeLoadState(true)
+ if (id) getGame(id, admin).then((result) => {
+ result.scrape = false
+ if (isMountedRef.current) gameState.set(result)
+ })
+
+ await getTags()
+
+ changeLoadState(false)
+ })()
+
+ return () => {
+ isMountedRef.current = false
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [admin])
+
+ const updateGame = async (id: string, data: Data) => {
+ try {
+ admin ? await agent.GamesAdmin.update(id, data) : await agent.Games.update(id, data)
+ } catch (err) {
+ return
+ }
+ }
+
+ const createGame = async (data: Data) => {
+ try {
+ admin ? await agent.GamesAdmin.create(data) : await agent.Games.create(data)
+ return true
+ } catch (err) {
+ return false
+ }
+ }
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault()
+
+ if ((gameId === '' && !game.scrape) || (gameId.length >= 1 && admin)) {
+ const shortDesc = game.shortDesc.replace(/"/g, '\'')
+ const summary = game.summary.replace(/"/g, '\'')
+ const reviews = game.reviews.replace(/"/g, '\'')
+ const windowsRecommended = game.systemRequirements.windows.recommended.replace(/"/g, '\'')
+ const windowsMinimum = game.systemRequirements.windows.minimum.replace(/"/g, '\'')
+ const macMinimum = game.systemRequirements.mac.minimum.replace(/"/g, '\'')
+ const macRecommended = game.systemRequirements.mac.recommended.replace(/"/g, '\'')
+ const linuxMinimum = game.systemRequirements.linux.minimum.replace(/"/g, '\'')
+ const linuxRecommended = game.systemRequirements.linux.recommended.replace(/"/g, '\'')
+ gameState.merge({
+ shortDesc: encode(shortDesc),
+ summary: encode(summary),
+ reviews: encode(reviews),
+ systemRequirements: {
+ windows: {
+ minimum: encode(windowsMinimum),
+ recommended: encode(windowsRecommended),
+ },
+ mac: {
+ minimum: encode(macMinimum),
+ recommended: encode(macRecommended),
+ },
+ linux: {
+ minimum: encode(linuxMinimum),
+ recommended: encode(linuxRecommended),
+ },
+ },
+ })
+ }
+
+
+ //@ts-ignore
+ if (event.nativeEvent.submitter.dataset.name === 'save-and-view') {
+ submitLoadingState.set(false)
+ moveOnState.set(true)
+ }
+
+ //@ts-ignore
+ if (event.nativeEvent.submitter.dataset.name === 'save') {
+ moveOnState.set(false)
+ submitLoadingState.set(true)
+ }
+
+ if (gameId.length > 1) {
+ // @ts-ignore
+ updateGame(game._id, game).then(async () => {
+ changeLoadState(true)
+ await retrieveTags()
+ // @ts-ignore
+ if (event.nativeEvent.submitter.dataset.name === 'save-and-view') {
+ moveOnState.set(() => false)
+ changeLoadState(false)
+ return history.push(`/games/${gameId}`)
+ }
+ await getGame(gameId, admin)
+ changeLoadState(false)
+ })
+ // if (submitLoading) submitLoadingState.set(false)
+ } else {
+ const id = new ObjectID().toHexString()
+ const newGame = {
+ ...game,
+ _id: id,
+ }
+
+ // @ts-ignore
+ createGame(newGame).then(async (data) => {
+ if (!data) {
+ changeLoadState(false)
+ return
+ // @ts-ignore
+ } else if (event.nativeEvent.submitter.dataset.name === 'save') {
+ changeLoadState(false)
+ return history.push(`/games/${newGame._id}/edit`)
+ // @ts-ignore
+ } else if (event.nativeEvent.submitter.dataset.name === 'save-and-view') {
+ changeLoadState(false)
+ return history.push(`/games/${newGame._id}`)
+ }
+ },
+ )
+ }
+ }
+
+ const handleInputChange = (name: string, value: Data[G]) => {
+ gameState.merge(() => ({[name]: value}))
+ }
+
+ const handleToggle = (name: G) => {
+ gameState.merge(() => ({[name]: !game[name]}))
+ }
+
+ const handleOSChange = (name: O) => {
+ gameState.os.merge(() => ({
+ [name]: !game.os[name],
+ }))
+ }
+
+ const handleSystemRequirements = (
+ name: S,
+ minRec: WML,
+ value: string,
+ ) => {
+ gameState.systemRequirements[name].merge(() => ({
+ [minRec]: value,
+ }))
+ }
+
+ const handleAccessedBy = (name: A, value: AccessedBy[A]) => {
+ gameState.accessedBy[0].merge(() => ({[name]: value}))
+ }
+
+ // @ts-ignore
+ const genres = stringArrayToSelectObject(game.genre)
+ // @ts-ignore
+ const store = stringArrayToSelectObject(game.accessedBy[0].store)
+ // @ts-ignore
+ const developer = stringArrayToSelectObject(game.developer)
+ // @ts-ignore
+ const publisher = stringArrayToSelectObject(game.publisher)
+ const screenshots = stringArrayToSelectObject(
+ game.screenshots.map((screenshot) => screenshot.full),
+ )
+
+ if (loaded) return
+
+ if (open) return
+
+ return (
+ <>
+
+ {id ? Editing {game.title} :
+ Add a Game to the Database}
+
+
+ >
+ )
+}
+
+export default GameForm
diff --git a/src/components/GamesDashboard.tsx b/src/components/GamesDashboard.tsx
new file mode 100644
index 0000000..280b6f8
--- /dev/null
+++ b/src/components/GamesDashboard.tsx
@@ -0,0 +1,78 @@
+// noinspection SpellCheckingInspection
+
+import React, { useEffect } from 'react'
+import { gameDashboardState } from '../stateManagement/gameState'
+import { loadingState } from '../stateManagement/loadingState'
+import { useHookstate } from '@hookstate/core'
+import { Box, Center, Container, Divider, Heading, SimpleGrid, VStack } from '@chakra-ui/react'
+import Pagination from './Pagination'
+import { NavLink } from 'react-router-dom'
+import { adminMode } from '../stateManagement/userState'
+import ActiveFilters from './ActiveFilters'
+import getWithFilters from "../componentUtils/getWithFilters";
+import { ratingState, steamRatingState } from "../stateManagement/ratingState";
+import GameThumbnailContextMenu from "./dashboardComponents/GameThumbnailContextMenu";
+import DashboardLoadingModal from "./dashboardComponents/DashboardLoadingModal";
+import ResetFilterButton from './helperComponents/ResetFilterButton'
+
+
+const GameDashboard = () => {
+ const loadedState = useHookstate(loadingState)
+ const loaded = loadedState.get()
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ const admin = useHookstate(adminMode).get()
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+
+
+ useEffect(() => {
+ getWithFilters(gamesState, loadedState, admin, games.count.currentPage, games.count.limit, games.searchParams, games.filters, useRatingState, useSteamRatingState)
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [games.success, admin])
+
+ return (
+ <>
+
+
+
+
+ {!loaded ? : games.count.totalGames === 0 ?
+
+
+ No Results Found...
+
+ {games.searchParams !== '' ?
+
+
+
+ :
+
+ Create a Game
+
+ }
+
+
+ :
+
+ {games.data.map((game) => (
+
+ ))}
+ }
+
+
+
+ >
+ )
+}
+
+export default GameDashboard
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
new file mode 100644
index 0000000..7c9a822
--- /dev/null
+++ b/src/components/Header.tsx
@@ -0,0 +1,252 @@
+import React, { useCallback, useEffect } from 'react'
+import { useHookstate } from '@hookstate/core'
+import { Link as RouteLink, useHistory, useLocation } from 'react-router-dom'
+import {loadedPreferences, loggedInUser, logout} from '../stateManagement/userState'
+import {
+ Box,
+ Button,
+ Flex,
+ Link,
+ HStack,
+ Stack,
+ useColorModeValue,
+ Menu,
+ MenuButton,
+ Avatar,
+ MenuList,
+ MenuItem,
+ MenuDivider,
+ Icon,
+ useColorMode,
+ Text,
+ Center,
+ VStack,
+ Tooltip,
+ Divider,
+ Heading,
+ Kbd,
+ Spacer,
+ useDisclosure,
+ useMediaQuery,
+} from '@chakra-ui/react'
+import { AddIcon, HamburgerIcon, Search2Icon, SettingsIcon } from '@chakra-ui/icons'
+import SearchModal from './SearchModal'
+import { openAndCloseSearchDialogue } from '../stateManagement/loadingState'
+import AdminMode from './authComponents/AdminMode'
+import MobileBar from './MobileBar'
+import FilterBar from './FilterBar'
+import { BiFilterAlt } from 'react-icons/bi'
+import {MdOutlinePowerSettingsNew} from "react-icons/md";
+
+export default function NavBar() {
+ const filterLoadingState = useHookstate(false)
+ const userState = useHookstate(loggedInUser)
+ const user = userState.get()
+ const setSearchModalOpen = useHookstate(openAndCloseSearchDialogue)
+ const searchModalOpen = setSearchModalOpen.get()
+ const { colorMode, toggleColorMode } = useColorMode()
+ const bgHeader = useColorModeValue('gray.100', 'gray.900')
+ const bgLink = useColorModeValue('gray.200', 'gray.700')
+ const location = useLocation()
+ const history = useHistory()
+ const { isOpen: mobileIsOpen, onOpen: mobileOnOpen, onClose: mobileOnClose } = useDisclosure()
+ const { isOpen: filterIsOpen, onOpen: filterOnOpen, onClose: filterOnClose } = useDisclosure()
+ const btnRef = React.useRef(null)
+ const [isSmallerThan768] = useMediaQuery('(max-width: 768px)')
+
+ const handleKeyPress = useCallback((event: any) => {
+ if (event.ctrlKey === true && event.key === ',') {
+ event.preventDefault()
+ setSearchModalOpen.set(true)
+ }
+
+ if (event.ctrlKey && event.key === '.' && location.pathname !== '/games') {
+ event.preventDefault()
+ history.push('/games')
+ }
+
+ if (event.ctrlKey && event.key === '/' && location.pathname !== '/games/create') {
+ event.preventDefault()
+ history.push('/games/create')
+ }
+ }, [setSearchModalOpen, location.pathname, history])
+
+ useEffect(() => {
+ document.addEventListener('keydown', handleKeyPress)
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyPress)
+ }
+ }, [handleKeyPress])
+
+ return (
+ <>
+ {searchModalOpen ? : ''}
+
+
+
+ Game Database
+
+
+
+ Games
+
+
+ Create Game
+
+
+
+
+
+ {isSmallerThan768 ?
+ <>
+
+
+ >
+ :
+
+
+ { user.role === 'admin' && }
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+ >
+ )
+}
diff --git a/src/components/Home.tsx b/src/components/Home.tsx
new file mode 100644
index 0000000..2400aae
--- /dev/null
+++ b/src/components/Home.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { VStack, Image, Button, Heading, Flex, Center } from '@chakra-ui/react'
+
+const Home = () => {
+ return (
+
+
+
+
+ Welcome to your Games Database
+
+
+
+
+ )
+}
+
+export default Home
diff --git a/src/components/LoadingModal.tsx b/src/components/LoadingModal.tsx
new file mode 100644
index 0000000..5303349
--- /dev/null
+++ b/src/components/LoadingModal.tsx
@@ -0,0 +1,21 @@
+import React from 'react'
+import { Center, CircularProgress } from '@chakra-ui/react'
+
+export default function LoadingModal() {
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/components/MobileBar.tsx b/src/components/MobileBar.tsx
new file mode 100644
index 0000000..246a897
--- /dev/null
+++ b/src/components/MobileBar.tsx
@@ -0,0 +1,134 @@
+import React, { MutableRefObject } from 'react'
+import {
+ Avatar, Button, Center, ColorMode,
+ Divider, Drawer,
+ DrawerBody,
+ DrawerCloseButton,
+ DrawerContent, DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay, Icon,
+ Image, Link,
+ Stack, Text,
+} from '@chakra-ui/react'
+import AdminMode from './authComponents/AdminMode'
+import { NavLink } from 'react-router-dom'
+import Search from './Search'
+import { AddIcon } from '@chakra-ui/icons'
+import { logout } from '../stateManagement/userState'
+import User from '../models/user'
+import { AiOutlineLogout } from 'react-icons/ai'
+import {useHookstate} from "@hookstate/core";
+import {searchCompletedState} from "../stateManagement/loadingState";
+import {gameDashboardState} from "../stateManagement/gameState";
+import ResetFilterButton from "./helperComponents/ResetFilterButton";
+
+interface Props {
+ mobileIsOpen: boolean
+ mobileOnClose: () => void
+ btnRef: MutableRefObject
+ bgLink: "gray.200" | "gray.700"
+ user: User
+ colorMode: ColorMode
+ toggleColorMode: () => void
+}
+
+function MobileBar({ mobileIsOpen, mobileOnClose, btnRef, bgLink, user }: Props) {
+ const searchCompleted = useHookstate(searchCompletedState)
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ Games
+
+
+ Create Game
+
+
+
+ {searchCompleted.get() && Games Found: {games.count.totalGames}}
+
+ {user.displayName}
+
+ Preferences
+
+
+ {/**/}
+ {/**/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default MobileBar
\ No newline at end of file
diff --git a/src/components/NoGames.tsx b/src/components/NoGames.tsx
new file mode 100644
index 0000000..1ee327d
--- /dev/null
+++ b/src/components/NoGames.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { Center, Flex, Heading, Button, VStack } from '@chakra-ui/react'
+
+export default function NoGames() {
+ return (
+
+
+
+ {You have not added any games yet.}
+
+
+
+
+ )
+}
diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx
new file mode 100644
index 0000000..40f8a56
--- /dev/null
+++ b/src/components/Pagination.tsx
@@ -0,0 +1,205 @@
+import React from 'react'
+import {
+ Flex,
+ Grid,
+ GridItem,
+ IconButton,
+ NumberDecrementStepper,
+ NumberIncrementStepper,
+ NumberInput,
+ NumberInputField,
+ NumberInputStepper,
+ Select,
+ Text,
+ Tooltip,
+ useColorModeValue,
+ useMediaQuery,
+} from '@chakra-ui/react'
+import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'
+import { useHookstate } from '@hookstate/core'
+import { gameDashboardState } from '../stateManagement/gameState'
+import { loadingState } from '../stateManagement/loadingState'
+import { adminMode } from '../stateManagement/userState'
+import getWithFilters from '../componentUtils/getWithFilters'
+import { ratingState, steamRatingState } from "../stateManagement/ratingState";
+
+export default function Pagination() {
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ const admin = useHookstate(adminMode).get()
+ const loadedState = useHookstate(loadingState)
+ const pageIndex = games.count.currentPage
+ const pageSize = games.count.limit
+ const pageCount = games.count.pages
+ const bg = useColorModeValue('gray.50', 'transparent')
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+ const [isSmallerThan768] = useMediaQuery('(max-width: 768px)')
+
+ const setPageSize = async (limit: number) => await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ games.count.currentPage,
+ limit,
+ games.searchParams,
+ games.filters,
+ useRatingState,
+ useSteamRatingState
+ )
+
+ const gotoPage = async (page: number) => await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ page,
+ games.count.limit,
+ games.searchParams,
+ games.filters,
+ useRatingState,
+ useSteamRatingState
+ )
+
+ const previousPage = async () => await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ games.pagination.prev.page,
+ games.count.limit,
+ games.searchParams,
+ games.filters,
+ useRatingState,
+ useSteamRatingState
+ )
+
+ const nextPage = async () => await getWithFilters(
+ gamesState,
+ loadedState,
+ admin,
+ games.pagination.next.page,
+ games.count.limit,
+ games.searchParams,
+ games.filters,
+ useRatingState,
+ useSteamRatingState
+ )
+
+ return (
+
+
+
+
+ gotoPage(0)}
+ isDisabled={pageIndex <= 1}
+ icon={}
+ mr={4}
+ />
+
+
+ }
+ />
+
+
+
+
+
+ Page{' '}
+
+ {pageIndex}
+ {' '}
+ of{' '}
+
+ {pageCount}
+
+
+
+ {!isSmallerThan768 &&
+
+ Go to Page:
+ {
+ const page = value ? Number(value) : 1
+ gotoPage(page)
+ }}
+ >
+
+
+
+
+
+
+
+
+ }
+
+ Count Per Page:
+ {
+ setPageSize(Number(e.target.value))
+ }}
+ >
+ {[10, 20, 30, 40, 50].map((pageSize) => (
+
+ ))}
+
+
+
+ Games Found: {games.count.totalGames}
+
+
+
+
+ = pageCount}
+ icon={}
+ />
+
+
+ gotoPage(pageCount)}
+ isDisabled={pageIndex >= pageCount}
+ icon={}
+ ml={4}
+ />
+
+
+
+
+ )
+}
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
new file mode 100644
index 0000000..f351a03
--- /dev/null
+++ b/src/components/Search.tsx
@@ -0,0 +1,88 @@
+import React, { FormEvent } from 'react'
+import { Button, FormControl, Input, InputGroup, InputRightElement, useColorModeValue } from '@chakra-ui/react'
+import { SearchIcon } from '@chakra-ui/icons'
+import { GameList } from '../models/game'
+import { useHookstate } from '@hookstate/core'
+import { gameDashboardState } from '../stateManagement/gameState'
+import {
+ loadingAndSaveState,
+ loadingState,
+ openAndCloseSearchDialogue, searchCompletedState, showFiltersModuleState,
+} from '../stateManagement/loadingState'
+import { useHistory } from 'react-router-dom'
+import { adminMode } from '../stateManagement/userState'
+import getWithFilters from '../componentUtils/getWithFilters'
+import {ChangeActiveFilterHeader} from "../componentUtils/changeActiveFilterHeader";
+import {ratingState, steamRatingState} from "../stateManagement/ratingState";
+
+export default function Search() {
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ const loadedState = useHookstate(loadingState)
+ const submitLoadingState = useHookstate(loadingAndSaveState)
+ const setShowFiltersModuleState = useHookstate(showFiltersModuleState)
+ const submitLoading = submitLoadingState.get()
+ const bg = useColorModeValue('gray.50', 'transparent')
+ const page = games.count.currentPage
+ const limit = games.count.limit
+ const setIsOpen = useHookstate(openAndCloseSearchDialogue)
+ const admin = useHookstate(adminMode).get()
+ const history = useHistory()
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+ const searchFinished= useHookstate(searchCompletedState)
+ const handleClose = () => {
+ setIsOpen.set(false)
+ }
+
+ const handleSubmit = async (event: FormEvent) => {
+ event.preventDefault()
+ submitLoadingState.set(true)
+ await getWithFilters(gamesState, loadedState, admin, page, limit, games.searchParams, games.filters, useRatingState, useSteamRatingState)
+ submitLoadingState.set(false)
+ games.searchParams.length > 0 && searchFinished.set(true)
+ handleClose()
+ return history.push('/games')
+ }
+
+ const handleInputChange = (name: G, value: string) => {
+ gamesState.merge(() => ({ [name]: value }))
+ ChangeActiveFilterHeader(games.filters, games.searchParams, setShowFiltersModuleState)
+ }
+
+
+ return (
+ <>
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx
new file mode 100644
index 0000000..c124d13
--- /dev/null
+++ b/src/components/SearchModal.tsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import {
+ Modal, ModalBody, ModalCloseButton, ModalContent,
+ ModalHeader,
+} from '@chakra-ui/react'
+import { useHookstate } from '@hookstate/core'
+import { openAndCloseSearchDialogue } from '../stateManagement/loadingState'
+import Search from './Search'
+
+export default function SearchModal() {
+ const setIsOpen = useHookstate(openAndCloseSearchDialogue)
+ const isOpen = setIsOpen.get()
+
+ const handleClose = () => {
+ setIsOpen.set(false)
+ }
+
+ return (
+
+
+ Search
+ handleClose()}/>
+
+
+
+
+
+ )
+}
diff --git a/src/components/authComponents/AdminMode.tsx b/src/components/authComponents/AdminMode.tsx
new file mode 100644
index 0000000..51122a2
--- /dev/null
+++ b/src/components/authComponents/AdminMode.tsx
@@ -0,0 +1,15 @@
+import React from 'react'
+import { Checkbox, Text } from '@chakra-ui/react'
+import { adminModeToggle, loggedInUser, adminMode } from '../../stateManagement/userState'
+import { useHookstate } from '@hookstate/core'
+
+export default function AdminMode() {
+ const user = useHookstate(loggedInUser).get()
+ const admin = useHookstate(adminMode).get()
+
+ return (
+ adminModeToggle()} isChecked={admin} isDisabled={!(user.role === 'admin')}>
+ Administrator Mode
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/authComponents/ForgotPassword.tsx b/src/components/authComponents/ForgotPassword.tsx
new file mode 100644
index 0000000..4348f7f
--- /dev/null
+++ b/src/components/authComponents/ForgotPassword.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import { useForm } from 'react-hook-form'
+import {
+ Container,
+ Button,
+ FormControl,
+ FormLabel,
+ Input,
+ useColorModeValue, FormErrorMessage, useToast,
+} from '@chakra-ui/react'
+import agent from '../../api/agent'
+import { IForgotPassword } from '../../models/user'
+
+export default function ForgotPassword() {
+ const bg = useColorModeValue('gray.50', 'transparent')
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isSubmitting, isDirty },
+ } = useForm()
+ const toast = useToast()
+
+ interface Message {
+ success: boolean
+ data: string
+ }
+
+ const onSubmit = handleSubmit(async (values) => {
+ const message = await agent.Account.forgot(values as IForgotPassword) as Message
+ toast({
+ title: 'Email Sent',
+ status: 'success',
+ description: message.data,
+ isClosable: true,
+ duration: 10000,
+ position: 'top'
+ })
+ })
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/authComponents/LoginForm.tsx b/src/components/authComponents/LoginForm.tsx
new file mode 100644
index 0000000..5e5ba8f
--- /dev/null
+++ b/src/components/authComponents/LoginForm.tsx
@@ -0,0 +1,94 @@
+import React from 'react'
+import { useForm } from 'react-hook-form'
+import { UserFormValues } from '../../models/user'
+import { login } from '../../stateManagement/userState'
+import {
+ Container,
+ Button,
+ FormControl,
+ FormLabel,
+ Input,
+ useColorModeValue, HStack, Center, FormErrorMessage,
+} from '@chakra-ui/react'
+import { Link } from 'react-router-dom'
+
+export default function LoginForm() {
+ const bg = useColorModeValue('gray.50', 'transparent')
+ const {
+ register,
+ handleSubmit,
+ setError,
+ formState: { errors, isSubmitting, isDirty },
+ } = useForm()
+
+ const onSubmit = handleSubmit(async (data) =>
+ await login(data as UserFormValues).catch(() =>
+ [
+ {
+ type: 'server',
+ name: 'password',
+ message: 'Invalid Email or Password',
+ },
+ ].forEach(({ name, type, message }) => {
+ setError(name, { type, message })
+ }),
+ ),
+ )
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/authComponents/ResetPassword.tsx b/src/components/authComponents/ResetPassword.tsx
new file mode 100644
index 0000000..fea30b1
--- /dev/null
+++ b/src/components/authComponents/ResetPassword.tsx
@@ -0,0 +1,120 @@
+import React, { useRef } from 'react'
+import { useForm } from 'react-hook-form'
+import { IResetPassword } from '../../models/user'
+import {
+ Button,
+ Container,
+ FormControl,
+ FormLabel,
+ Input, useToast, useColorModeValue, FormErrorMessage,
+} from '@chakra-ui/react'
+import agent from '../../api/agent'
+import { useHistory, useParams } from 'react-router-dom'
+
+interface Message {
+ success: boolean
+ data: string
+}
+
+interface URL {
+ token: string
+}
+
+export default function ResetPassword() {
+ const {
+ register,
+ handleSubmit,
+ watch,
+ formState: { errors, isSubmitting, isDirty },
+ } = useForm()
+ const { token } = useParams()
+ const history = useHistory()
+ const toast = useToast()
+ const password = useRef({})
+ password.current = watch('password', '')
+
+ const onSubmit = handleSubmit(async (data) => {
+ const message = await agent.Account.reset(token, data as IResetPassword) as Message
+ if (message.success) {
+ setTimeout(() => history.push('/login'), 3000)
+ toast({
+ title: 'Password Reset',
+ status: 'success',
+ description: 'Your password has been reset. You will be redirected to login automatically',
+ isClosable: true,
+ duration: 5000,
+ position: 'top',
+ })
+ } else {
+ toast({
+ title: 'Password Not Reset',
+ status: 'error',
+ description: 'Something went wrong, please contact the website administrator',
+ isClosable: true,
+ duration: 10000,
+ position: 'top',
+ })
+ }
+ })
+
+ const bg = useColorModeValue('gray.50', 'transparent')
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/dashboardComponents/DashboardLoadingModal.tsx b/src/components/dashboardComponents/DashboardLoadingModal.tsx
new file mode 100644
index 0000000..e8e35a3
--- /dev/null
+++ b/src/components/dashboardComponents/DashboardLoadingModal.tsx
@@ -0,0 +1,16 @@
+import React from 'react'
+import { Center, CircularProgress } from '@chakra-ui/react'
+
+export default function DashboardLoadingModal() {
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/components/dashboardComponents/GameThumbnail.tsx b/src/components/dashboardComponents/GameThumbnail.tsx
new file mode 100644
index 0000000..58fc6bd
--- /dev/null
+++ b/src/components/dashboardComponents/GameThumbnail.tsx
@@ -0,0 +1,44 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import { Data } from '../../models/game'
+import { Link as ChakraLink, Flex, Heading, Image, LinkBox, LinkOverlay, Spacer, Text } from '@chakra-ui/react'
+import GameRating from "../detailsComponents/GameRating";
+import SteamAndDeckLogo from "../helperComponents/SteamAndDeckLogo";
+
+interface Props {
+ game: Data
+ onToggle: () => void
+ playState: string
+}
+
+export default function GameThumbnail({ onToggle, game, playState }: Props) {
+
+ const gameLink = game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 ? game.steamId : game._id
+ return (
+ <>
+
+
+
+
+
+ {game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`}
+
+ Play Status: {playState}
+
+
+
+
+ {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 && (
+
+
+
+ )}
+
+ >
+ )
+}
diff --git a/src/components/dashboardComponents/GameThumbnailContextMenu.tsx b/src/components/dashboardComponents/GameThumbnailContextMenu.tsx
new file mode 100644
index 0000000..d9bfce6
--- /dev/null
+++ b/src/components/dashboardComponents/GameThumbnailContextMenu.tsx
@@ -0,0 +1,69 @@
+import React, { useState } from 'react';
+import { Data } from "../../models/game";
+import { ContextMenu } from 'chakra-ui-contextmenu';
+import { MenuItem, MenuList } from '@chakra-ui/menu';
+import GameThumbnail from "./GameThumbnail";
+import { Box, MenuGroup, useDisclosure } from "@chakra-ui/react";
+import GameThumbnailSlide from "./GameThumbnailSlide";
+import { playStatusValues } from "../../componentUtils/SelectValues";
+import agent from "../../api/agent";
+import { Link } from "react-router-dom";
+
+interface Props {
+ game: Data
+}
+
+const GameThumbnailContextMenu = ({ game }: Props) => {
+ const { isOpen, onToggle } = useDisclosure()
+ const [playState, setPlayState] = useState(game.accessedBy[0].playStatus)
+ const gameLink = game.steamId.length > 1 ? game.steamId : game._id
+
+ const rightClick = async (label: string, playStatus: string) => {
+ try {
+ setPlayState(playStatus)
+ await agent.Games.playStatus(game._id, {
+ accessedBy: [{
+ playStatus
+ }]
+ })
+ } catch (err) {
+ console.error(`${label}: ${err}`)
+ }
+ }
+
+ let mql = window.matchMedia('(max-width: 768px)');
+
+ return (
+ key={game._id} renderMenu={() => (
+
+
+
+ {playStatusValues.map((value, index) => )}
+
+
+ )}>
+ {ref => <>
+
+
+
+ {(!mql.matches) && }
+ >
+ }
+
+ );
+};
+
+export default GameThumbnailContextMenu;
diff --git a/src/components/dashboardComponents/GameThumbnailSlide.tsx b/src/components/dashboardComponents/GameThumbnailSlide.tsx
new file mode 100644
index 0000000..f3160ba
--- /dev/null
+++ b/src/components/dashboardComponents/GameThumbnailSlide.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import {Box, Container, Heading, HStack, Slide} from "@chakra-ui/react";
+import {Data} from "../../models/game";
+import ImageSlider from "../detailsComponents/ImageSlider";
+
+interface Props {
+ isOpen: boolean
+ game: Data
+}
+
+const GameThumbnailSlide = ({isOpen, game}: Props) => {
+ return (
+
+
+
+
+
+
+ {game.title}
+ {game.shortDesc}
+
+
+
+
+
+ );
+};
+
+export default GameThumbnailSlide;
diff --git a/src/components/detailsComponents/Features.tsx b/src/components/detailsComponents/Features.tsx
new file mode 100644
index 0000000..8aafcfd
--- /dev/null
+++ b/src/components/detailsComponents/Features.tsx
@@ -0,0 +1,321 @@
+import React, { useState as reactUseState } from 'react'
+import { Badge, Box, Divider, Flex, Heading, Link, Text } from '@chakra-ui/react'
+import { Data } from '../../models/game'
+import { gameDashboardState } from '../../stateManagement/gameState'
+import { ImmutableObject, useHookstate } from '@hookstate/core'
+import { loadingState, showFiltersModuleState } from '../../stateManagement/loadingState'
+import { useHistory } from 'react-router-dom'
+import { adminMode } from '../../stateManagement/userState'
+import getWithFilters from '../../componentUtils/getWithFilters'
+import { ChangeActiveFilterHeader } from "../../componentUtils/changeActiveFilterHeader";
+import GameRating from "./GameRating";
+import GamePlayStatus from "./GamePlayStatus";
+import { ratingState, steamRatingState } from "../../stateManagement/ratingState";
+import { BiSave } from "react-icons/bi";
+import { BsPencil } from "react-icons/bs";
+
+interface Props {
+ game: ImmutableObject
+}
+
+export default function Features({ game }: Props) {
+ const gamesState = useHookstate(gameDashboardState)
+ const games = gamesState.get()
+ const setShowFiltersModuleState = useHookstate(showFiltersModuleState)
+ const loadedState = useHookstate(loadingState)
+ const admin = useHookstate(adminMode).get()
+ const history = useHistory()
+ const [editMode, setEditMode] = reactUseState(false)
+ const [playState, setPlayState] = reactUseState(game.accessedBy[0].playStatus);
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+
+ const handleOnClick = async () => {
+ await ChangeActiveFilterHeader(games.filters, games.searchParams, setShowFiltersModuleState)
+ await getWithFilters(gamesState, loadedState, admin, games.count.currentPage, games.count.limit, games.searchParams, games.filters, useRatingState, useSteamRatingState)
+ return history.push('/games')
+ }
+
+ return (
+
+ Features and Information
+
+
+
+
+ Rating:
+
+
+
+
+
+
+
+
+ {game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
+ (<>
+
+
+
+
+ Steam ID:
+
+
+
+
+ {game.steamId}
+
+
+
+ >)}
+ {game.series.length > 1 &&
+ (<>
+
+
+
+
+ Series:
+
+
+
+
+ {
+ gamesState.filters.series.set(game.series)
+ handleOnClick()
+ }} pt="1">{game.series}
+
+
+
+ >)}
+
+
+
+
+ Play Status:
+
+
+
+
+
+ {editMode ?
+
+
+ setEditMode(!editMode)}>
+
+
+
+ :
+
+ {
+ gamesState.filters.playStatus.set(game.accessedBy[0].playStatus)
+ handleOnClick()
+ }} pt="1">
+ {playState}
+
+ setEditMode(!editMode)}>
+
+
+
+ }
+
+
+
+
+
+
+
+
+ Genre:
+
+
+
+
+ {game.genre.map((genre, index) => [
+ index > 0 && ", ",
+ {
+ gamesState.filters.genre.set(genre)
+ handleOnClick()
+ }}>
+ {genre}
+
+ ])}
+
+
+
+
+
+
+
+ Operating Systems:
+
+
+
+
+
+ {Object.keys(game.os).filter(os => {
+ //@ts-ignore
+ return game.os[os]
+ }).map((os, index) => {
+ const osStr = []
+ index > 0 && osStr.push(', ')
+ if (os === 'mac') osStr.push( {
+ gamesState.filters.os.set(os)
+ handleOnClick()
+ }}>Mac OSX)
+ else if (os === 'ios') osStr.push( {
+ gamesState.filters.os.set(os)
+ handleOnClick()
+ }}>iOS)
+ else osStr.push( {
+ gamesState.filters.os.set(os)
+ handleOnClick()
+ }}>{os.charAt(0).toUpperCase() + os.slice(1)})
+ return osStr
+ })}
+
+
+
+
+
+
+
+
+ Store:
+
+
+
+
+ {game.accessedBy[0].store.map((store, index) => [
+ index > 0 && ", ",
+ {
+ gamesState.filters.store.set(store)
+ handleOnClick()
+ }}>
+ {store}
+
+ ])}
+
+
+
+
+
+
+
+ Developer:
+
+
+
+
+ {game.developer.map((developer, index) => [
+ index > 0 && ", ",
+ {
+ gamesState.filters.developer.set(developer)
+ handleOnClick()
+ }}>
+ {developer}
+
+ ])}
+
+
+
+
+
+
+
+ Publisher:
+
+
+
+
+ {game.publisher.map((publisher, index) => [
+ index > 0 && ", ",
+ {
+ gamesState.filters.publisher.set(publisher)
+ handleOnClick()
+ }}>
+ {publisher}
+
+ ])}
+
+
+
+
+
+
+
+ Controller Support:
+
+
+
+
+ {
+ gamesState.filters.controller.set(game.controller)
+ handleOnClick()
+ }} pt="1">{game.controller}
+
+
+
+
+
+
+
+ Release Date:
+
+
+
+
+ {game.releaseDate}
+
+
+
+
+
+
+
+ Soundtrack:
+
+
+
+
+ {
+ gamesState.filters.soundtrack.set(game.accessedBy[0].soundtrack)
+ handleOnClick()
+ }} pt="1">{game.accessedBy[0].soundtrack}
+
+
+
+
+
+
+
+ Does it work with Intel?:
+
+
+
+
+ {
+ gamesState.filters.intel.set(game.intel)
+ handleOnClick()
+ }} pt="1">{game.intel}
+
+
+
+
+
+
+
+ Does it work with WINE?:
+
+
+
+
+ {
+ gamesState.filters.wine.set(game.wine)
+ handleOnClick()
+ }} pt="1">{game.wine}
+
+
+
+
+ )
+}
diff --git a/src/components/detailsComponents/GamePlayStatus.tsx b/src/components/detailsComponents/GamePlayStatus.tsx
new file mode 100644
index 0000000..5b51395
--- /dev/null
+++ b/src/components/detailsComponents/GamePlayStatus.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { Data } from '../../models/game'
+import agent from "../../api/agent";
+import {playStatusValues} from "../../componentUtils/SelectValues";
+import {Select} from "chakra-react-select";
+
+
+interface Props {
+ game: Data
+ playState: string
+ setPlayState: any
+}
+
+const GamePlayStatus = ({game, playState, setPlayState}: Props) => {
+
+ const handleClick = async (label: string, playStatus: string) => {
+ try {
+ setPlayState(playStatus)
+ await agent.Games.playStatus(game._id, {
+ accessedBy: [{
+ playStatus
+ }]
+ })
+ } catch (err) {
+ console.error(`${label}: ${err}`)
+ }
+ }
+
+ return {
+ if(e === null) return
+ handleClick(e.value, e.label)
+ }}
+ size={'sm'}
+ options={playStatusValues}
+ chakraStyles={{
+ container: (provided) => ({
+ ...provided,
+ width: "150px"
+ })
+ }}
+ />
+ };
+
+ export default GamePlayStatus;
diff --git a/src/components/detailsComponents/GameRating.tsx b/src/components/detailsComponents/GameRating.tsx
new file mode 100644
index 0000000..7b89b7c
--- /dev/null
+++ b/src/components/detailsComponents/GameRating.tsx
@@ -0,0 +1,52 @@
+import React, {useState} from 'react';
+import {Data} from '../../models/game'
+import Rating from 'react-rating'
+import agent from "../../api/agent";
+import {Flex, Link} from "@chakra-ui/react";
+import {IoGameController, IoGameControllerOutline} from "react-icons/io5";
+import {BiReset} from "react-icons/bi";
+
+
+interface Props {
+ game: Data
+ size: string
+}
+
+const GameRating = ({game, size}: Props) => {
+ const steamRating = game.steamRating
+ const [rating, setRating] = useState(game.accessedBy[0].rating);
+
+ const handleClick = async (rating: number) => {
+ try {
+ setRating(rating)
+ await agent.Games.rating(game._id, {
+ accessedBy: [{
+ rating
+ }]
+ })
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ const finalRating = rating === 0 ? steamRating : rating
+
+ return (
+
+ {/*@ts-ignore*/}
+ }
+ fullSymbol={}
+ />
+ handleClick(0)}>
+
+ )
+};
+
+export default GameRating;
diff --git a/src/components/detailsComponents/ImageSlider.tsx b/src/components/detailsComponents/ImageSlider.tsx
new file mode 100644
index 0000000..681caad
--- /dev/null
+++ b/src/components/detailsComponents/ImageSlider.tsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Data } from '../../models/game'
+import { Carousel } from 'react-responsive-carousel'
+import { Image } from '@chakra-ui/react'
+import { ImmutableObject } from '@hookstate/core'
+
+interface Props {
+ game: ImmutableObject
+ width: string
+}
+
+export default function ImageSlider({ game, width }: Props) {
+ const { frontImage, screenshots } = game
+ const newFrontImage = { id: 500, full: frontImage, thumbnail: frontImage }
+ const combinedImages = [newFrontImage, ...screenshots]
+
+ return (
+
+ {combinedImages.map(({ id, full }) => (
+
+ ),
+ )}
+
+ )
+}
diff --git a/src/components/detailsComponents/Summary.tsx b/src/components/detailsComponents/Summary.tsx
new file mode 100644
index 0000000..420c529
--- /dev/null
+++ b/src/components/detailsComponents/Summary.tsx
@@ -0,0 +1,35 @@
+import React from 'react'
+import { Data } from '../../models/game'
+import { Interweave } from 'interweave'
+import { Divider, Box, Heading } from '@chakra-ui/react'
+import { ImmutableObject } from '@hookstate/core'
+
+interface Props {
+ game: ImmutableObject
+}
+
+export default function Summary({ game }: Props) {
+ const { summary, reviews } = game
+ return (
+
+ {reviews.length >= 1 && (
+ <>
+ Reviews
+
+
+ >
+ )}
+ = 1 ? '10' : ''} as="h1">About This Game
+
+
+
+ )
+}
diff --git a/src/components/detailsComponents/SummaryModal.tsx b/src/components/detailsComponents/SummaryModal.tsx
new file mode 100644
index 0000000..d7f1a16
--- /dev/null
+++ b/src/components/detailsComponents/SummaryModal.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'
+import Summary from './Summary'
+import { ImmutableObject } from '@hookstate/core'
+import { Data } from '../../models/game'
+
+
+interface Props {
+ isOpen: boolean,
+ onClose: () => void,
+ game: ImmutableObject
+}
+
+const SummaryModal = ({ isOpen, onClose, game }: Props) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default SummaryModal
diff --git a/src/components/detailsComponents/SystemRequirementsMenu.tsx b/src/components/detailsComponents/SystemRequirementsMenu.tsx
new file mode 100644
index 0000000..e750089
--- /dev/null
+++ b/src/components/detailsComponents/SystemRequirementsMenu.tsx
@@ -0,0 +1,70 @@
+import React from 'react'
+import { TabList, Tab, TabPanel, TabPanels, Tabs, Heading, Box } from '@chakra-ui/react'
+import { Data } from '../../models/game'
+import { Interweave } from 'interweave'
+import { ImmutableObject } from '@hookstate/core'
+
+interface Props {
+ game: ImmutableObject
+}
+
+export default function SystemRequirements({ game }: Props) {
+ const { windows, mac, linux } = game.systemRequirements
+ let winRec, macMin, macRec, linMin, linRec
+ windows.recommended.length > 1 ? winRec = false : winRec = true
+ mac.minimum.length > 1 ? macMin = false : macMin = true
+ mac.recommended.length > 1 ? macRec = false : macRec = true
+ linux.minimum.length > 1 ? linMin = false : linMin = true
+ linux.recommended.length > 1 ? linRec = false : linRec = true
+
+ return (
+
+ System Requirements
+
+
+ Windows
+ Mac
+ Linux
+
+
+
+
+
+ Minimum
+ Recommended
+
+
+ {}
+ {}
+
+
+
+
+
+
+ Minimum
+ Recommended
+
+
+ {}
+ {}
+
+
+
+
+
+
+ Minimum
+ Recommended
+
+
+ {}
+ {}
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/detailsComponents/SystemRequirementsModal.tsx b/src/components/detailsComponents/SystemRequirementsModal.tsx
new file mode 100644
index 0000000..38de6f5
--- /dev/null
+++ b/src/components/detailsComponents/SystemRequirementsModal.tsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay } from '@chakra-ui/react'
+import { ImmutableObject } from '@hookstate/core'
+import { Data } from '../../models/game'
+import SystemRequirements from './SystemRequirementsMenu'
+
+
+interface Props {
+ isOpen: boolean,
+ onClose: () => void,
+ game: ImmutableObject
+}
+
+const SummaryModal = ({ isOpen, onClose, game }: Props) => {
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default SummaryModal
diff --git a/src/components/formFields/CheckboxInput.tsx b/src/components/formFields/CheckboxInput.tsx
new file mode 100644
index 0000000..4449eb4
--- /dev/null
+++ b/src/components/formFields/CheckboxInput.tsx
@@ -0,0 +1,26 @@
+import React from 'react'
+import { Checkbox, Text } from '@chakra-ui/react'
+
+interface Props {
+ label: string
+ textField: string
+ checked: boolean
+ defaultIsChecked: boolean
+ handleOSChange: any
+}
+
+const CheckboxInput = ({ label, textField, checked, defaultIsChecked, handleOSChange }: Props) => {
+ return (
+ {
+ handleOSChange(textField)
+ }}
+ name={textField}
+ spacing="1rem"
+ >{label}
+ )
+}
+
+export default CheckboxInput
\ No newline at end of file
diff --git a/src/components/formFields/CreatableSelectInput.tsx b/src/components/formFields/CreatableSelectInput.tsx
new file mode 100644
index 0000000..78f457e
--- /dev/null
+++ b/src/components/formFields/CreatableSelectInput.tsx
@@ -0,0 +1,46 @@
+import React from 'react'
+import { Box, FormControl, FormLabel } from '@chakra-ui/react'
+import { CreatableSelect } from 'chakra-react-select'
+
+interface Props {
+ label: string
+ textField: string
+ value: any
+ bg: string
+ handleInputChange: any
+ options: any
+}
+
+const CreatableSelectInput = ({ label, textField, value, bg, handleInputChange, options}: Props) => {
+ return (
+
+ {label}
+
+ {
+ if (values) {
+ const valueArray = values.reduce((prev: string[], curr: any) => {
+ prev.push(curr.value)
+ return prev
+ }, [])
+ handleInputChange(textField, valueArray)
+ }
+ }}
+ value={value}
+ options={options}
+ />
+
+
+ )
+}
+
+export default CreatableSelectInput
\ No newline at end of file
diff --git a/src/components/formFields/SelectInput.tsx b/src/components/formFields/SelectInput.tsx
new file mode 100644
index 0000000..1cd5507
--- /dev/null
+++ b/src/components/formFields/SelectInput.tsx
@@ -0,0 +1,42 @@
+import React from 'react'
+import {Box, FormControl, FormLabel} from '@chakra-ui/react'
+import {Select} from 'chakra-react-select'
+
+interface Props {
+ label: string
+ textField: string
+ field: string
+ bg: string
+ handleAccessedBy: any
+ options: { value: string, label: string }[]
+ width?: string
+}
+
+const SelectInput = ({label, textField, field, bg, handleAccessedBy, options, width = ''}: Props) => {
+ return (
+
+ {label}
+
+ {
+ if (values) handleAccessedBy(textField, values.value)
+ }}
+ options={options}
+ value={{
+ value: field,
+ label: field,
+ }}
+ />
+
+
+ )
+}
+
+export default SelectInput
\ No newline at end of file
diff --git a/src/components/formFields/SingleFieldInput.tsx b/src/components/formFields/SingleFieldInput.tsx
new file mode 100644
index 0000000..5c31eff
--- /dev/null
+++ b/src/components/formFields/SingleFieldInput.tsx
@@ -0,0 +1,37 @@
+import React from 'react'
+import { FormControl, FormLabel, Input } from '@chakra-ui/react'
+import { Data } from '../../models/game'
+
+interface Props {
+ label: string
+ textField: string
+ field: string
+ bg: string
+ handleInputChange: (name: string, value: Data[G]) => void
+ required: boolean
+}
+
+const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => {
+
+ return (
+
+ {label}
+ {
+ handleInputChange(textField, e.target.value)
+ }}
+ />
+
+ )
+}
+
+export default SingleFieldInput
\ No newline at end of file
diff --git a/src/components/formFields/SingleFieldNumericInput.tsx b/src/components/formFields/SingleFieldNumericInput.tsx
new file mode 100644
index 0000000..4348e1c
--- /dev/null
+++ b/src/components/formFields/SingleFieldNumericInput.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import { FormControl, FormLabel, Input } from '@chakra-ui/react'
+import { Data } from '../../models/game'
+
+interface Props {
+ label: string
+ textField: string
+ field: string
+ bg: string
+ handleInputChange: (name: string, value: Data[G]) => void
+ required: boolean
+}
+
+const SingleFieldNumericInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => {
+
+ return (
+
+ {label}
+ {
+ handleInputChange(textField, e.target.value)
+ }}
+ />
+
+ )
+}
+
+export default SingleFieldNumericInput
\ No newline at end of file
diff --git a/src/components/formFields/SystemRequirementsTextareaInput.tsx b/src/components/formFields/SystemRequirementsTextareaInput.tsx
new file mode 100644
index 0000000..27130b8
--- /dev/null
+++ b/src/components/formFields/SystemRequirementsTextareaInput.tsx
@@ -0,0 +1,41 @@
+import React, {useMemo, useRef} from 'react'
+import {FormControl, FormLabel} from '@chakra-ui/react'
+import JoditEditor from "jodit-react";
+
+interface Props {
+ label: string
+ textField: string
+ field: string
+ bg: string
+ handleSystemRequirements: any
+ rows: number
+}
+
+const SystemRequirementsTextareaInput = ({label, textField, field, handleSystemRequirements}: Props) => {
+ const arrText = textField.split('.')
+ const editor = useRef(null);
+
+ const config = useMemo(() =>
+ ({
+ readonly: false, // all options from https://xdsoft.net/jodit/doc/,
+ theme: 'dark',
+ }),
+ []
+ );
+
+ return (
+
+ {label}
+ {
+ handleSystemRequirements(arrText[1], arrText[2], value)
+ }}
+ />
+
+ )
+}
+
+export default SystemRequirementsTextareaInput
\ No newline at end of file
diff --git a/src/components/formFields/TextareaInput.tsx b/src/components/formFields/TextareaInput.tsx
new file mode 100644
index 0000000..d350f6f
--- /dev/null
+++ b/src/components/formFields/TextareaInput.tsx
@@ -0,0 +1,42 @@
+import React, {useMemo, useRef} from 'react'
+import { FormControl, FormLabel } from '@chakra-ui/react'
+import { Data } from '../../models/game'
+import JoditEditor from "jodit-react";
+
+interface Props {
+ label: string
+ textField: string
+ field: string
+ bg: string
+ handleInputChange: (name: string, value: Data[G]) => void
+ rows: number
+}
+
+const TextareaInput = ({ label, textField, field, handleInputChange }: Props) => {
+ const editor = useRef(null);
+
+ const config = useMemo( () =>
+ ({
+ readonly: false, // all options from https://xdsoft.net/jodit/doc/,
+ theme: 'dark',
+ }),
+ []
+ );
+
+
+ return (
+
+ {label}
+ {
+ handleInputChange(textField, value)
+ }}
+ />
+
+ )
+}
+
+export default TextareaInput
\ No newline at end of file
diff --git a/src/components/helperComponents/ResetFilterButton.tsx b/src/components/helperComponents/ResetFilterButton.tsx
new file mode 100644
index 0000000..f1bcd2c
--- /dev/null
+++ b/src/components/helperComponents/ResetFilterButton.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import { gameDashboardState } from '../../stateManagement/gameState'
+import { useHookstate } from "@hookstate/core";
+import { loadingState, searchCompletedState, showFiltersModuleState } from "../../stateManagement/loadingState";
+import getWithFilters from "../../componentUtils/getWithFilters";
+import { adminMode } from "../../stateManagement/userState";
+import { Button } from '@chakra-ui/react';
+import { ratingState, steamRatingState } from "../../stateManagement/ratingState";
+
+interface Props {
+ text?: string
+ size?: string
+ variant?: string
+ colorScheme?: string
+}
+
+const ResetFilterButton = ({ text = "Reset Search and Filters", size = 'md', variant = 'solid', colorScheme = 'gray' }: Props) => {
+ const gameState = useHookstate(gameDashboardState)
+ const games = gameState.get()
+ const admin = useHookstate(adminMode).get()
+ const loadedState = useHookstate(loadingState)
+ const useRatingState = useHookstate(ratingState).get()
+ const useSteamRatingState = useHookstate(steamRatingState).get()
+ const searchComplete = useHookstate(searchCompletedState)
+ const setShowFiltersModuleState = useHookstate(showFiltersModuleState)
+
+ const resetFilters = async () => {
+ const searchParams = ''
+ const finalFilter = {
+ controller: "",
+ developer: "",
+ genre: "",
+ intel: "",
+ os: "",
+ playStatus: "",
+ publisher: "",
+ series: "",
+ soundtrack: "",
+ store: "",
+ wine: "",
+ rating: "",
+ steamRating: "",
+ }
+ searchComplete.set(false)
+ setShowFiltersModuleState.set(false)
+
+ return await getWithFilters(
+ gameState,
+ loadedState,
+ admin,
+ games.count.currentPage,
+ games.count.limit,
+ searchParams,
+ finalFilter,
+ useRatingState,
+ useSteamRatingState,
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default ResetFilterButton;
diff --git a/src/components/helperComponents/SteamAndDeckLogo.tsx b/src/components/helperComponents/SteamAndDeckLogo.tsx
new file mode 100644
index 0000000..602216d
--- /dev/null
+++ b/src/components/helperComponents/SteamAndDeckLogo.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import {Box} from "@chakra-ui/react";
+
+interface ISteamAndDeckLogo {
+ size: string
+}
+
+const SteamAndDeckLogo = (props: ISteamAndDeckLogo) => {
+ return (
+
+
+ SteamAndDeckLogo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SteamAndDeckLogo;
diff --git a/src/components/userComponents/CreateUser.tsx b/src/components/userComponents/CreateUser.tsx
new file mode 100644
index 0000000..8e3cc58
--- /dev/null
+++ b/src/components/userComponents/CreateUser.tsx
@@ -0,0 +1,161 @@
+import React, { useRef } from 'react'
+import { useForm } from 'react-hook-form'
+import { UserFormValues } from '../../models/user'
+import { createUser } from '../../stateManagement/userState'
+import {
+ Alert,
+ AlertDescription,
+ AlertIcon,
+ Button,
+ Container,
+ FormControl, FormErrorMessage,
+ FormLabel,
+ Input, useColorModeValue,
+} from '@chakra-ui/react'
+
+export default function CreateUser() {
+ const {
+ register,
+ handleSubmit,
+ setError,
+ watch,
+ formState: { errors, isSubmitting, isDirty },
+ } = useForm()
+ const password = useRef({})
+ password.current = watch('password', '')
+
+ const onSubmit = handleSubmit(async (data) => {
+ data.role = 'user'
+ return await createUser(data as UserFormValues).catch(() =>
+ [
+ {
+ type: 'server',
+ name: 'password',
+ message: 'Invalid Email or Password',
+ },
+ ].forEach(({ name, type, message }) => {
+ setError(name, { type, message })
+ })
+ )
+ })
+
+ const bg = useColorModeValue('gray.50', 'transparent')
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/userComponents/EditUser.tsx b/src/components/userComponents/EditUser.tsx
new file mode 100644
index 0000000..f6d5486
--- /dev/null
+++ b/src/components/userComponents/EditUser.tsx
@@ -0,0 +1,178 @@
+import React, { useEffect } from 'react'
+import { Controller, useForm } from 'react-hook-form'
+import { IForgotPassword, UserPreferences, UserUpdateValues } from '../../models/user'
+import { loadedPreferences, loggedInUser, updateUserPreferences } from '../../stateManagement/userState'
+import {
+ Button,
+ Container, Flex,
+ FormControl,
+ FormLabel,
+ Heading,
+ Input,
+ Stack, Switch,
+ useColorModeValue,
+ useToast,
+} from '@chakra-ui/react'
+import { useHookstate } from '@hookstate/core'
+import agent from '../../api/agent'
+import { useHistory } from 'react-router-dom'
+
+export default function EditUser() {
+ const userState = useHookstate(loggedInUser)
+ const user = userState.get()
+ const passwordLoading = useHookstate(false)
+ const passwordDisabled = useHookstate(false)
+ const preferencesState = useHookstate(loadedPreferences)
+ const preferences = preferencesState.get()
+ const {
+ register,
+ control,
+ handleSubmit,
+ reset,
+ setValue,
+ formState: { isSubmitting, isDirty },
+ } = useForm({
+ defaultValues: {
+ name: user.name,
+ displayName: user.displayName,
+ email: user.email,
+ theme: preferences.theme === "dark",
+ stickyNav: preferences.stickyNav
+ },
+ })
+ const history = useHistory()
+
+ useEffect(() => {
+ reset(user)
+ return
+ }, [user.name !== ''])
+
+ const toast = useToast()
+
+ interface Message {
+ success: boolean
+ data: string
+ }
+
+ const onSubmit = handleSubmit(async (data) => {
+ let theme: "light" | "dark"
+ data.theme ? theme = "dark" : theme = "light"
+ const newUserData: UserUpdateValues = {...data, role: user.role, avatar: user.avatar}
+ const newPreferenceData: UserPreferences = { id: preferences.id, theme, stickyNav: data.stickyNav }
+ await agent.Account.update(newUserData)
+ await updateUserPreferences(newPreferenceData)
+ return history.push('/edit-user')
+ })
+
+ const engageChangePassword = async (values: IForgotPassword) => {
+ const message = await agent.Account.forgot(values as IForgotPassword) as Message
+ toast({
+ title: 'Email Sent',
+ status: 'success',
+ description: message.data,
+ isClosable: true,
+ duration: 10000,
+ position: 'top'
+ })
+ }
+
+ useEffect(() => {
+ setValue("theme", preferences.theme === "dark")
+ }, [setValue, preferences.theme]);
+
+ useEffect(() => {
+ setValue("stickyNav", preferences.stickyNav)
+ }, [setValue, preferences.stickyNav])
+
+ const bg = useColorModeValue('gray.50', 'transparent')
+
+ return (
+
+ Edit {user.displayName}
+
+
+ )
+}
diff --git a/src/components/userComponents/UserProfile.tsx b/src/components/userComponents/UserProfile.tsx
new file mode 100644
index 0000000..c698112
--- /dev/null
+++ b/src/components/userComponents/UserProfile.tsx
@@ -0,0 +1,74 @@
+import React from 'react'
+import {
+ Box,
+ Button,
+ Container,
+ Grid,
+ GridItem,
+ Heading,
+ Image,
+ Table,
+ TableContainer,
+ Tbody,
+ Td,
+ Text,
+ Tr,
+ useColorModeValue
+} from '@chakra-ui/react'
+import {useHookstate} from '@hookstate/core'
+import {loggedInUser} from '../../stateManagement/userState'
+import {Link} from 'react-router-dom'
+
+const UserProfile = () => {
+ const user = useHookstate(loggedInUser).get()
+ const bg = useColorModeValue('white', 'gray.700')
+
+ return (
+
+ User Profile
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Display Name: |
+ {user.displayName} |
+
+
+ Full Name: |
+ {user.name} |
+
+
+ Email: |
+ {user.email} |
+
+
+ Role: |
+ {user.role} |
+
+
+
+
+
+
+
+
+ )
+}
+
+export default UserProfile
diff --git a/src/errors/NotFound.tsx b/src/errors/NotFound.tsx
new file mode 100644
index 0000000..eb554c0
--- /dev/null
+++ b/src/errors/NotFound.tsx
@@ -0,0 +1,40 @@
+import React, {lazy, Suspense} from 'react'
+import {Link} from 'react-router-dom'
+import {Button, Heading} from '@chakra-ui/react'
+import LoadingModal from "../components/LoadingModal";
+
+const styles = {
+ retroFontHeader: {
+ fontFamily: ['RetroGaming', 'sans-serif'].join(','),
+ fontSize: '4.3rem',
+ marginLeft: '5rem',
+ marginTop: '5rem'
+ },
+ retroFontBody: {
+ fontFamily: ['RetroGaming', 'sans-serif'].join(','),
+ },
+}
+
+export default function NotFound() {
+ const Video = lazy(() => import("./Video"))
+ return (
+ <>
+
+ 404 :(
+
+
+ It looks like you hit a{' '}
+ {DEAD} end!
+
+
+ }>
+
+
+ >
+ )
+}
diff --git a/src/errors/SomethingWentWrong.tsx b/src/errors/SomethingWentWrong.tsx
new file mode 100644
index 0000000..34e8203
--- /dev/null
+++ b/src/errors/SomethingWentWrong.tsx
@@ -0,0 +1,42 @@
+import React, {lazy, Suspense} from 'react'
+import {Link} from 'react-router-dom'
+import {Button, Heading} from '@chakra-ui/react'
+import LoadingModal from "../components/LoadingModal";
+
+const styles = {
+ retroFontHeader: {
+ fontFamily: ['RetroGaming', 'sans-serif'].join(','),
+ fontSize: '4.3rem',
+ marginLeft: '5rem',
+ marginTop: '5rem'
+ },
+ retroFontBody: {
+ fontFamily: ['RetroGaming', 'sans-serif'].join(','),
+ },
+}
+
+export default function SomethingWentWrong() {
+ const Video = lazy(() => import("./Video"))
+ return (
+ <>
+
+ Something Went Wrong
+
+
+ {WE APOLOGIZE!}
+ Please contact the website administrator!
+
+
+ }>
+
+
+ >
+ )
+}
diff --git a/src/errors/TestError.tsx b/src/errors/TestError.tsx
new file mode 100644
index 0000000..1523ffe
--- /dev/null
+++ b/src/errors/TestError.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import axios from 'axios'
+import { Container, Button, Heading } from '@chakra-ui/react'
+
+export default function TestErrors() {
+ const baseUrl = 'http://localhost:5000/api/'
+
+ function handleNotFound() {
+ axios
+ .get(baseUrl + 'buggy/not-found')
+ .catch((err) => console.error(err.response))
+ }
+
+ function handleBadRequest() {
+ axios
+ .get(baseUrl + 'buggy/bad-request')
+ .catch((err) => console.error(err.response))
+ }
+
+ function handleServerError() {
+ axios
+ .get(baseUrl + 'buggy/server-error')
+ .catch((err) => console.error(err.response))
+ }
+
+ function handleUnauthorised() {
+ axios
+ .get(baseUrl + 'buggy/unauthorised')
+ .catch((err) => console.error(err.response))
+ }
+
+ function handleBadGuid() {
+ axios
+ .get(baseUrl + 'activities/notaguid')
+ .catch((err) => console.error(err.response))
+ }
+
+ function handleValidationError() {
+ axios
+ .post(baseUrl + 'activities', {})
+ .catch((err) => console.error(err.reponse))
+ }
+
+ return (
+ <>
+
+ Test Error component
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/errors/Video.tsx b/src/errors/Video.tsx
new file mode 100644
index 0000000..2d4588d
--- /dev/null
+++ b/src/errors/Video.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+import game_over_mp4 from "../resources/video/game_over.mp4";
+import game_over_webm from "../resources/video/game_over.webm";
+import game_over_desktop from "../resources/images/game_over.jpg";
+import game_over_mobile from "../resources/images/game_over_mobile.jpg";
+
+
+export default function Video() {
+
+ return (
+
+ {window.innerWidth >= 895 ? (
+
+ ) : (
+
+ )}
+
+ )
+};
diff --git a/src/index.tsx b/src/index.tsx
new file mode 100644
index 0000000..31a322e
--- /dev/null
+++ b/src/index.tsx
@@ -0,0 +1,35 @@
+import "@hookstate/devtools";
+import React from "react";
+import {createRoot} from "react-dom/client";
+import App from "./App";
+import reportWebVitals from "./reportWebVitals";
+import "react-toastify/dist/ReactToastify.min.css";
+import "./styles/typography.css";
+import "./styles/videoBackground.css";
+import "./styles/overrides.css";
+import "react-responsive-carousel/lib/styles/carousel.min.css";
+import {Router} from "react-router-dom";
+import {createBrowserHistory} from "history";
+import {ChakraProvider, ColorModeScript} from "@chakra-ui/react";
+import theme from "./styles/theme";
+
+export const history = createBrowserHistory();
+
+const container = document.getElementById("root");
+const root = createRoot(container!);
+
+root.render(
+
+
+
+
+
+
+
+
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();
diff --git a/src/models/defaultGame.ts b/src/models/defaultGame.ts
new file mode 100644
index 0000000..4c3cb88
--- /dev/null
+++ b/src/models/defaultGame.ts
@@ -0,0 +1,54 @@
+const defaultGame = {
+ _id: '',
+ title: '',
+ series: '',
+ steamId: '',
+ slug: '',
+ frontImage: '',
+ screenshots: [],
+ genre: [],
+ os: {
+ windows: true,
+ mac: false,
+ linux: false,
+ android: false,
+ ios: false
+ },
+ wine: 'Not Tested',
+ controller: 'No Controller Support',
+ developer: [],
+ publisher: [],
+ releaseDate: '',
+ shortDesc: '',
+ reviews: '',
+ summary: '',
+ intel: 'Not Tested',
+ steamRating: 0,
+ systemRequirements: {
+ windows: {
+ minimum: '',
+ recommended: '',
+ },
+ mac: {
+ minimum: '',
+ recommended: '',
+ },
+ linux: {
+ minimum: '',
+ recommended: '',
+ }
+ },
+ accessedBy: [{
+ user: '',
+ store: ['Steam'],
+ playStatus: 'Never Played',
+ soundtrack: 'No',
+ rating: 0,
+ }],
+ createDate: '',
+ lastUpdateDate: '',
+ lastModifiedBy: '',
+ scrape: true,
+ }
+
+ export default defaultGame
\ No newline at end of file
diff --git a/src/models/defaultUser.ts b/src/models/defaultUser.ts
new file mode 100644
index 0000000..f9c8d00
--- /dev/null
+++ b/src/models/defaultUser.ts
@@ -0,0 +1,11 @@
+const defaultUser = {
+ _id: '',
+ name: '',
+ displayName: '',
+ email: '',
+ avatar: '',
+ token: '',
+ role: '',
+}
+
+export default defaultUser
diff --git a/src/models/game.ts b/src/models/game.ts
new file mode 100644
index 0000000..78e92af
--- /dev/null
+++ b/src/models/game.ts
@@ -0,0 +1,100 @@
+export interface GameList {
+ success: boolean
+ searchParams: string
+ count: Count
+ pagination: Pagination
+ filters: Filters
+ data: Data[]
+}
+
+export default interface Game {
+ success: boolean
+ data: Data
+}
+
+export interface Data {
+ _id: string
+ title: string
+ series: string
+ steamId: string
+ slug: string
+ frontImage: string
+ screenshots: { id: number; full: string; thumbnail: string }[]
+ genre: string[]
+ os: Os
+ wine: string
+ controller: string
+ developer: string[]
+ publisher: string[]
+ releaseDate: string
+ shortDesc: string
+ reviews: string
+ summary: string
+ intel: string
+ steamRating: number
+ systemRequirements: SystemRequirements
+ accessedBy: AccessedBy[]
+ createDate: string
+ lastUpdateDate: string
+ lastModifiedBy: string
+ scrape: boolean
+}
+
+export interface Os {
+ windows: boolean
+ mac: boolean
+ linux: boolean
+ android: boolean
+ ios: boolean
+}
+export interface SystemRequirements {
+ windows: WindowsOrMacOrLinux
+ mac: WindowsOrMacOrLinux
+ linux: WindowsOrMacOrLinux
+}
+export interface WindowsOrMacOrLinux {
+ minimum: string
+ recommended: string
+}
+
+export interface AccessedBy {
+ user: string
+ store: string[]
+ playStatus: string
+ soundtrack: string
+ rating: number
+}
+
+export interface Pagination {
+ prev: PrevNext
+ next: PrevNext
+
+}
+
+export interface PrevNext {
+ page: number
+}
+
+export interface Count {
+ gamesPerPage: number
+ totalGames: number
+ currentPage: number
+ pages: number
+ limit: number
+}
+
+export interface Filters {
+ series: string
+ playStatus: string
+ genre: string
+ os: string
+ store: string
+ developer: string
+ publisher: string
+ controller: string
+ soundtrack: string
+ intel: string
+ wine: string
+ rating: string
+ steamRating: string
+}
diff --git a/src/models/tags.ts b/src/models/tags.ts
new file mode 100644
index 0000000..0ad36bf
--- /dev/null
+++ b/src/models/tags.ts
@@ -0,0 +1,378 @@
+export const genreTags = [
+ { value: "1980s", label: "1980s" },
+ { value: "1990's", label: "1990's" },
+ { value: "2.5D", label: "2.5D" },
+ { value: "2D", label: "2D" },
+ { value: "2D Fighter", label: "2D Fighter" },
+ { value: "2D Platformer", label: "2D Platformer" },
+ { value: "360 Video", label: "360 Video" },
+ { value: "3D", label: "3D" },
+ { value: "3D Fighter", label: "3D Fighter" },
+ { value: "3D Platformer", label: "3D Platformer" },
+ { value: "3D Vision", label: "3D Vision" },
+ { value: "4 Player Local", label: "4 Player Local" },
+ { value: "4X", label: "4X" },
+ { value: "6DOF", label: "6DOF" },
+ { value: "Abstract", label: "Abstract" },
+ { value: "Action", label: "Action" },
+ { value: "Action Roguelike", label: "Action Roguelike" },
+ { value: "Action RPG", label: "Action RPG" },
+ { value: "Action-Adventure", label: "Action-Adventure" },
+ { value: "Addictive", label: "Addictive" },
+ { value: "Adventure", label: "Adventure" },
+ { value: "Agriculture", label: "Agriculture" },
+ { value: "Aliens", label: "Aliens" },
+ { value: "Alternate History", label: "Alternate History" },
+ { value: "America", label: "America" },
+ { value: "Anime", label: "Anime" },
+ { value: "Arcade", label: "Arcade" },
+ { value: "Archery", label: "Archery" },
+ { value: "Arena Shooter", label: "Arena Shooter" },
+ { value: "Artificial Intelligence", label: "Artificial Intelligence" },
+ { value: "Assassin", label: "Assassin" },
+ { value: "Asymmetric VR", label: "Asymmetric VR" },
+ { value: "Asynchronous Multiplayer", label: "Asynchronous Multiplayer" },
+ { value: "Atmospheric", label: "Atmospheric" },
+ { value: "ATV", label: "ATV" },
+ { value: "Auto Battler", label: "Auto Battler" },
+ { value: "Automation", label: "Automation" },
+ { value: "Automobile Sim", label: "Automobile Sim" },
+ { value: "Base Building", label: "Base Building" },
+ { value: "Baseball", label: "Baseball" },
+ { value: "Based on a Novel", label: "Based on a Novel" },
+ { value: "Basketball", label: "Basketball" },
+ { value: "Batman", label: "Batman" },
+ { value: "Battle Royale", label: "Battle Royale" },
+ { value: "Beat 'em up", label: "Beat 'em up" },
+ { value: "Beautiful", label: "Beautiful" },
+ { value: "Bikes", label: "Bikes" },
+ { value: "BMX", label: "BMX" },
+ { value: "Board Game", label: "Board Game" },
+ { value: "Bowling", label: "Bowling" },
+ { value: "Boxing", label: "Boxing" },
+ { value: "Building", label: "Building" },
+ { value: "Bullet Hell", label: "Bullet Hell" },
+ { value: "Bullet Time", label: "Bullet Time" },
+ { value: "Capitalism", label: "Capitalism" },
+ { value: "Card Battler", label: "Card Battler" },
+ { value: "Card Game", label: "Card Game" },
+ { value: "Cartoon", label: "Cartoon" },
+ { value: "Cartoony", label: "Cartoony" },
+ { value: "Casual", label: "Casual" },
+ { value: "Cats", label: "Cats" },
+ { value: "Character Action Game", label: "Character Action Game" },
+ { value: "Character Customization", label: "Character Customization" },
+ { value: "Chess", label: "Chess" },
+ { value: "Choices Matter", label: "Choices Matter" },
+ { value: "Choose Your Own Adventure", label: "Choose Your Own Adventure" },
+ { value: "Cinematic", label: "Cinematic" },
+ { value: "City Builder", label: "City Builder" },
+ { value: "Class-Based", label: "Class-Based" },
+ { value: "Classic", label: "Classic" },
+ { value: "Clicker", label: "Clicker" },
+ { value: "Co-op", label: "Co-op" },
+ { value: "Co-op Campaign", label: "Co-op Campaign" },
+ { value: "Cold War", label: "Cold War" },
+ { value: "Collectathon", label: "Collectathon" },
+ { value: "Colony Sim", label: "Colony Sim" },
+ { value: "Colorful", label: "Colorful" },
+ { value: "Combat", label: "Combat" },
+ { value: "Combat Racing", label: "Combat Racing" },
+ { value: "Comedy", label: "Comedy" },
+ { value: "Comic Book", label: "Comic Book" },
+ { value: "Competitive", label: "Competitive" },
+ { value: "Conspiracy", label: "Conspiracy" },
+ { value: "Conversation", label: "Conversation" },
+ { value: "Crafting", label: "Crafting" },
+ { value: "Crime", label: "Crime" },
+ { value: "CRPG", label: "CRPG" },
+ { value: "Cult Classic", label: "Cult Classic" },
+ { value: "Cute", label: "Cute" },
+ { value: "Cyberpunk", label: "Cyberpunk" },
+ { value: "Cycling", label: "Cycling" },
+ { value: "Dark", label: "Dark" },
+ { value: "Dark Comedy", label: "Dark Comedy" },
+ { value: "Dark Fantasy", label: "Dark Fantasy" },
+ { value: "Dating Sim", label: "Dating Sim" },
+ { value: "Deckbuilding", label: "Deckbuilding" },
+ { value: "Demons", label: "Demons" },
+ { value: "Destruction", label: "Destruction" },
+ { value: "Detective", label: "Detective" },
+ { value: "Difficult", label: "Difficult" },
+ { value: "Dinosaurs", label: "Dinosaurs" },
+ { value: "Diplomacy", label: "Diplomacy" },
+ { value: "Documentary", label: "Documentary" },
+ { value: "Dog", label: "Dog" },
+ { value: "Dragons", label: "Dragons" },
+ { value: "Drama", label: "Drama" },
+ { value: "Driving", label: "Driving" },
+ { value: "Dungeon Crawler", label: "Dungeon Crawler" },
+ { value: "Dungeons & Dragons", label: "Dungeons & Dragons" },
+ { value: "Dynamic Narration", label: "Dynamic Narration" },
+ { value: "Economy", label: "Economy" },
+ { value: "Education", label: "Education" },
+ { value: "Emotional", label: "Emotional" },
+ { value: "Epic", label: "Epic" },
+ { value: "Episodic", label: "Episodic" },
+ { value: "eSports", label: "eSports" },
+ { value: "Experience", label: "Experience" },
+ { value: "Experimental", label: "Experimental" },
+ { value: "Exploration", label: "Exploration" },
+ { value: "Faith", label: "Faith" },
+ { value: "Family Friendly", label: "Family Friendly" },
+ { value: "Fantasy", label: "Fantasy" },
+ { value: "Farming Sim", label: "Farming Sim" },
+ { value: "Fast-Paced", label: "Fast-Paced" },
+ { value: "Feature Film", label: "Feature Film" },
+ { value: "Female Protagonist", label: "Female Protagonist" },
+ { value: "Fighting", label: "Fighting" },
+ { value: "First-Person", label: "First-Person" },
+ { value: "Fishing", label: "Fishing" },
+ { value: "Flight", label: "Flight" },
+ { value: "FMV", label: "FMV" },
+ { value: "Football", label: "Football" },
+ { value: "Foreign", label: "Foreign" },
+ { value: "FPS", label: "FPS" },
+ { value: "Funny", label: "Funny" },
+ { value: "Futuristic", label: "Futuristic" },
+ { value: "Gambling", label: "Gambling" },
+ { value: "Game Development", label: "Game Development" },
+ { value: "Games Workshop", label: "Games Workshop" },
+ { value: "God Game", label: "God Game" },
+ { value: "Golf", label: "Golf" },
+ { value: "Gothic", label: "Gothic" },
+ { value: "Grand Strategy", label: "Grand Strategy" },
+ { value: "Great Soundtrack", label: "Great Soundtrack" },
+ { value: "Grid-Based Movement", label: "Grid-Based Movement" },
+ { value: "Gun Customization", label: "Gun Customization" },
+ { value: "Hack and Slash", label: "Hack and Slash" },
+ { value: "Hacking", label: "Hacking" },
+ { value: "Hand-drawn", label: "Hand-drawn" },
+ { value: "Heist", label: "Heist" },
+ { value: "Hero Shooter", label: "Hero Shooter" },
+ { value: "Hex Grid", label: "Hex Grid" },
+ { value: "Hidden Object", label: "Hidden Object" },
+ { value: "Historical", label: "Historical" },
+ { value: "Hockey", label: "Hockey" },
+ { value: "Horror", label: "Horror" },
+ { value: "Horses", label: "Horses" },
+ { value: "Hunting", label: "Hunting" },
+ { value: "Idler", label: "Idler" },
+ { value: "Illuminati", label: "Illuminati" },
+ { value: "Immersive Sim", label: "Immersive Sim" },
+ { value: "Indie", label: "Indie" },
+ {
+ value: "Intentionally Awkward Controls ",
+ label: "Intentionally Awkward Controls ",
+ },
+ { value: "Interactive Fiction", label: "Interactive Fiction" },
+ { value: "Inventory Management", label: "Inventory Management" },
+ { value: "Investigation", label: "Investigation" },
+ { value: "Isometric", label: "Isometric" },
+ { value: "Jet", label: "Jet" },
+ { value: "JRPG", label: "JRPG" },
+ { value: "Lara Croft", label: "Lara Croft" },
+ { value: "LEGO", label: "LEGO" },
+ { value: "Lemmings", label: "Lemmings" },
+ { value: "Level Editor", label: "Level Editor" },
+ { value: "LGBTQ+", label: "LGBTQ+" },
+ { value: "Life Sim", label: "Life Sim" },
+ { value: "Linear", label: "Linear" },
+ { value: "Local Co-Op", label: "Local Co-Op" },
+ { value: "Local Multiplayer", label: "Local Multiplayer" },
+ { value: "Logic", label: "Logic" },
+ { value: "Loot", label: "Loot" },
+ { value: "Looter Shooter", label: "Looter Shooter" },
+ { value: "Lore-Rich", label: "Lore-Rich" },
+ { value: "Lovecraftian", label: "Lovecraftian" },
+ { value: "Magic", label: "Magic" },
+ { value: "Management", label: "Management" },
+ { value: "Mars", label: "Mars" },
+ { value: "Martial Arts", label: "Martial Arts" },
+ { value: "Massively Multiplayer", label: "Massively Multiplayer" },
+ { value: "Masterpiece", label: "Masterpiece" },
+ { value: "Match 3", label: "Match 3" },
+ { value: "Mechs", label: "Mechs" },
+ { value: "Medical Sim", label: "Medical Sim" },
+ { value: "Medieval", label: "Medieval" },
+ { value: "Memes", label: "Memes" },
+ { value: "Metroidvania", label: "Metroidvania" },
+ { value: "Military", label: "Military" },
+ { value: "Mini Golf", label: "Mini Golf" },
+ { value: "Minigames", label: "Minigames" },
+ { value: "Minimalist", label: "Minimalist" },
+ { value: "Mining", label: "Mining" },
+ { value: "MMORPG", label: "MMORPG" },
+ { value: "MOBA", label: "MOBA" },
+ { value: "Mod", label: "Mod" },
+ { value: "Moddable", label: "Moddable" },
+ { value: "Modern", label: "Modern" },
+ { value: "Motocross", label: "Motocross" },
+ { value: "Motorbike", label: "Motorbike" },
+ { value: "Movie", label: "Movie" },
+ { value: "Multiplayer", label: "Multiplayer" },
+ { value: "Multiple Endings", label: "Multiple Endings" },
+ { value: "Music", label: "Music" },
+ {
+ value: "Music-Based Procedural Generation",
+ label: "Music-Based Procedural Generation",
+ },
+ { value: "Mystery", label: "Mystery" },
+ { value: "Mystery Dungeon", label: "Mystery Dungeon" },
+ { value: "Mythology", label: "Mythology" },
+ { value: "Narration", label: "Narration" },
+ { value: "Nature", label: "Nature" },
+ { value: "Naval", label: "Naval" },
+ { value: "Naval Combat", label: "Naval Combat" },
+ { value: "Ninja", label: "Ninja" },
+ { value: "Noir", label: "Noir" },
+ { value: "Nonlinear", label: "Nonlinear" },
+ { value: "Offroad", label: "Offroad" },
+ { value: "Old School", label: "Old School" },
+ { value: "On-Rails Shooter", label: "On-Rails Shooter" },
+ { value: "Online Co-Op", label: "Online Co-Op" },
+ { value: "Open World", label: "Open World" },
+ { value: "Open World Survival Craft", label: "Open World Survival Craft" },
+ { value: "Otome", label: "Otome" },
+ { value: "Outbreak Sim", label: "Outbreak Sim" },
+ { value: "Parkour", label: "Parkour" },
+ { value: "Party-Based RPG", label: "Party-Based RPG" },
+ { value: "Perma Death", label: "Perma Death" },
+ { value: "Philosophical", label: "Philosophical" },
+ { value: "Physics", label: "Physics" },
+ { value: "Pinball", label: "Pinball" },
+ { value: "Pirates", label: "Pirates" },
+ { value: "Pixel Graphics", label: "Pixel Graphics" },
+ { value: "Platformer", label: "Platformer" },
+ { value: "Point & Click", label: "Point & Click" },
+ { value: "Political", label: "Political" },
+ { value: "Political Sim", label: "Political Sim" },
+ { value: "Politics", label: "Politics" },
+ { value: "Pool", label: "Pool" },
+ { value: "Post-apocalyptic", label: "Post-apocalyptic" },
+ { value: "Precision Platformer", label: "Precision Platformer" },
+ { value: "Procedural Generation", label: "Procedural Generation" },
+ { value: "Programming", label: "Programming" },
+ { value: "Psychedelic", label: "Psychedelic" },
+ { value: "Psychological", label: "Psychological" },
+ { value: "Puzzle", label: "Puzzle" },
+ { value: "PvE", label: "PvE" },
+ { value: "PvP", label: "PvP" },
+ { value: "Quick-Time Events", label: "Quick-Time Events" },
+ { value: "Racing", label: "Racing" },
+ { value: "Real Time Tactics", label: "Real Time Tactics" },
+ { value: "Real-Time", label: "Real-Time" },
+ { value: "Real-Time with Pause", label: "Real-Time with Pause" },
+ { value: "Realistic", label: "Realistic" },
+ { value: "Relaxing", label: "Relaxing" },
+ { value: "Remake", label: "Remake" },
+ { value: "Replay Value", label: "Replay Value" },
+ { value: "Resource Management", label: "Resource Management" },
+ { value: "Retro", label: "Retro" },
+ { value: "Rhythm", label: "Rhythm" },
+ { value: "Robots", label: "Robots" },
+ { value: "Roguelike", label: "Roguelike" },
+ { value: "Roguelite", label: "Roguelite" },
+ { value: "Roguevania", label: "Roguevania" },
+ { value: "Romance", label: "Romance" },
+ { value: "Rome", label: "Rome" },
+ { value: "RPG", label: "RPG" },
+ { value: "RTS", label: "RTS" },
+ { value: "Runner", label: "Runner" },
+ { value: "Sailing", label: "Sailing" },
+ { value: "Sandbox", label: "Sandbox" },
+ { value: "Satire", label: "Satire" },
+ { value: "Sci-fi", label: "Sci-fi" },
+ { value: "Science", label: "Science" },
+ { value: "Score Attack", label: "Score Attack" },
+ { value: "Sequel", label: "Sequel" },
+ { value: "Shoot 'Em Up", label: "Shoot 'Em Up" },
+ { value: "Shooter", label: "Shooter" },
+ { value: "Short", label: "Short" },
+ { value: "Side Scroller", label: "Side Scroller" },
+ { value: "Silent Protagonist", label: "Silent Protagonist" },
+ { value: "Simulation", label: "Simulation" },
+ { value: "Singleplayer", label: "Singleplayer" },
+ { value: "Skateboarding", label: "Skateboarding" },
+ { value: "Skating", label: "Skating" },
+ { value: "Skiing", label: "Skiing" },
+ { value: "Sniper", label: "Sniper" },
+ { value: "Snow", label: "Snow" },
+ { value: "Snowboarding", label: "Snowboarding" },
+ { value: "Soccer", label: "Soccer" },
+ { value: "Sokoban", label: "Sokoban" },
+ { value: "Solitaire", label: "Solitaire" },
+ { value: "Souls-like", label: "Souls-like" },
+ { value: "Soundtrack", label: "Soundtrack" },
+ { value: "Space", label: "Space" },
+ { value: "Space Sim", label: "Space Sim" },
+ { value: "Spectacle fighter", label: "Spectacle fighter" },
+ { value: "Spelling", label: "Spelling" },
+ { value: "Split Screen", label: "Split Screen" },
+ { value: "Sports", label: "Sports" },
+ { value: "Star Wars", label: "Star Wars" },
+ { value: "Stealth", label: "Stealth" },
+ { value: "Steampunk", label: "Steampunk" },
+ { value: "Story Rich", label: "Story Rich" },
+ { value: "Strategy", label: "Strategy" },
+ { value: "Strategy RPG", label: "Strategy RPG" },
+ { value: "Stylized", label: "Stylized" },
+ { value: "Submarine", label: "Submarine" },
+ { value: "Superhero", label: "Superhero" },
+ { value: "Supernatural", label: "Supernatural" },
+ { value: "Surreal", label: "Surreal" },
+ { value: "Survival", label: "Survival" },
+ { value: "Survival Horror", label: "Survival Horror" },
+ { value: "Swordplay", label: "Swordplay" },
+ { value: "Tabletop", label: "Tabletop" },
+ { value: "Tactical", label: "Tactical" },
+ { value: "Tactical RPG", label: "Tactical RPG" },
+ { value: "Tanks", label: "Tanks" },
+ { value: "Team-Based", label: "Team-Based" },
+ { value: "Tennis", label: "Tennis" },
+ { value: "Text-Based", label: "Text-Based" },
+ { value: "Third Person", label: "Third Person" },
+ { value: "Third-Person Shooter", label: "Third-Person Shooter" },
+ { value: "Thriller", label: "Thriller" },
+ { value: "Time Attack", label: "Time Attack" },
+ { value: "Time Management", label: "Time Management" },
+ { value: "Time Manipulation", label: "Time Manipulation" },
+ { value: "Time Travel", label: "Time Travel" },
+ { value: "Top-Down", label: "Top-Down" },
+ { value: "Top-Down Shooter", label: "Top-Down Shooter" },
+ { value: "Tower Defense", label: "Tower Defense" },
+ { value: "Trading", label: "Trading" },
+ { value: "Trading Card Game", label: "Trading Card Game" },
+ { value: "Traditional Roguelike", label: "Traditional Roguelike" },
+ { value: "Trains", label: "Trains" },
+ { value: "Transhumanism", label: "Transhumanism" },
+ { value: "Transportation", label: "Transportation" },
+ { value: "Trivia", label: "Trivia" },
+ { value: "Turn-Based", label: "Turn-Based" },
+ { value: "Turn-Based Combat", label: "Turn-Based Combat" },
+ { value: "Turn-Based Strategy", label: "Turn-Based Strategy" },
+ { value: "Turn-Based Tactics", label: "Turn-Based Tactics" },
+ { value: "Tutorial", label: "Tutorial" },
+ { value: "Twin Stick Shooter", label: "Twin Stick Shooter" },
+ { value: "Typing", label: "Typing" },
+ { value: "Underground", label: "Underground" },
+ { value: "Underwater", label: "Underwater" },
+ { value: "Unforgiving", label: "Unforgiving" },
+ { value: "Vampire", label: "Vampire" },
+ { value: "Vehicular Combat", label: "Vehicular Combat" },
+ { value: "Villain Protagonist", label: "Villain Protagonist" },
+ { value: "Visual Novel", label: "Visual Novel" },
+ { value: "Voxel", label: "Voxel" },
+ { value: "VR", label: "VR" },
+ { value: "Walking Simulator", label: "Walking Simulator" },
+ { value: "War", label: "War" },
+ { value: "Wargame", label: "Wargame" },
+ { value: "Warhammer 40K", label: "Warhammer 40K" },
+ { value: "Werewolves", label: "Werewolves" },
+ { value: "Western", label: "Western" },
+ { value: "Word Game", label: "Word Game" },
+ { value: "World War I", label: "World War I" },
+ { value: "World War II", label: "World War II" },
+ { value: "Wrestling", label: "Wrestling" },
+];
+
diff --git a/src/models/user.ts b/src/models/user.ts
new file mode 100644
index 0000000..54133cb
--- /dev/null
+++ b/src/models/user.ts
@@ -0,0 +1,44 @@
+export interface UserAPI {
+ success: boolean
+ data: User
+}
+
+export default interface User {
+ _id: string
+ name: string
+ displayName: string
+ email: string
+ avatar: string
+ role: string
+ token: string
+}
+
+export interface UserPreferences {
+ id: string
+ theme: 'light' | 'dark'
+ stickyNav: boolean
+}
+
+export interface UserFormValues {
+ email: string
+ password: string
+ name?: string
+ displayName?: string
+ role?: string
+}
+
+export interface UserUpdateValues {
+ email: string
+ name: string
+ displayName: string
+ role: string
+ avatar: string
+}
+
+export interface IForgotPassword {
+ email: string
+}
+
+export interface IResetPassword {
+ password: string
+}
\ No newline at end of file
diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts
new file mode 100644
index 0000000..91061fa
--- /dev/null
+++ b/src/react-app-env.d.ts
@@ -0,0 +1,74 @@
+
+///
+///
+
+declare namespace NodeJS {
+ interface Process{
+ env: ProcessEnv
+ }
+ interface ProcessEnv {
+ /**
+ * By default, there are two modes in Vite:
+ *
+ * * `development` is used by vite and vite serve
+ * * `production` is used by vite build
+ *
+ * You can overwrite the default mode used for a command by passing the --mode option flag.
+ *
+ */
+ readonly NODE_ENV: 'development' | 'production'
+ }
+}
+
+declare var process: NodeJS.Process
+
+declare module '*.gif' {
+ const src: string
+ export default src
+}
+
+declare module '*.jpg' {
+ const src: string
+ export default src
+}
+
+declare module '*.jpeg' {
+ const src: string
+ export default src
+}
+
+declare module '*.png' {
+ const src: string
+ export default src
+}
+
+declare module '*.webp' {
+ const src: string
+ export default src
+}
+
+declare module '*.svg' {
+ import * as React from 'react'
+
+ export const ReactComponent: React.FunctionComponent & { title?: string }>
+
+ const src: string;
+ export default src
+}
+
+declare module '*.module.css' {
+ const classes: { readonly [key: string]: string }
+ export default classes
+}
+
+declare module '*.module.scss' {
+ const classes: { readonly [key: string]: string }
+ export default classes
+}
+
+declare module '*.module.sass' {
+ const classes: { readonly [key: string]: string }
+ export default classes
+}
diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts
new file mode 100644
index 0000000..49a2a16
--- /dev/null
+++ b/src/reportWebVitals.ts
@@ -0,0 +1,15 @@
+import { ReportHandler } from 'web-vitals';
+
+const reportWebVitals = (onPerfEntry?: ReportHandler) => {
+ if (onPerfEntry && onPerfEntry instanceof Function) {
+ import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry);
+ getFID(onPerfEntry);
+ getFCP(onPerfEntry);
+ getLCP(onPerfEntry);
+ getTTFB(onPerfEntry);
+ });
+ }
+};
+
+export default reportWebVitals;
diff --git a/src/resources/fonts/retrogaming.ttf b/src/resources/fonts/retrogaming.ttf
new file mode 100644
index 0000000..0dca996
Binary files /dev/null and b/src/resources/fonts/retrogaming.ttf differ
diff --git a/src/resources/images/game_over.jpg b/src/resources/images/game_over.jpg
new file mode 100644
index 0000000..7216b51
Binary files /dev/null and b/src/resources/images/game_over.jpg differ
diff --git a/src/resources/images/game_over_mobile.jpg b/src/resources/images/game_over_mobile.jpg
new file mode 100644
index 0000000..8583f98
Binary files /dev/null and b/src/resources/images/game_over_mobile.jpg differ
diff --git a/src/resources/video/game_over.mp4 b/src/resources/video/game_over.mp4
new file mode 100644
index 0000000..d1f6362
Binary files /dev/null and b/src/resources/video/game_over.mp4 differ
diff --git a/src/resources/video/game_over.webm b/src/resources/video/game_over.webm
new file mode 100644
index 0000000..b9e5fa4
Binary files /dev/null and b/src/resources/video/game_over.webm differ
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 0000000..8f2609b
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';
diff --git a/src/stateManagement/gameState.ts b/src/stateManagement/gameState.ts
new file mode 100644
index 0000000..97c587a
--- /dev/null
+++ b/src/stateManagement/gameState.ts
@@ -0,0 +1,37 @@
+import defaultGame from '../models/defaultGame'
+import Game, { Data, GameList } from '../models/game'
+import { hookstate } from '@hookstate/core'
+
+export const gameDashboardState = hookstate({
+ success: false,
+ count: {
+ gamesPerPage: 0,
+ totalGames: 0,
+ currentPage: 0,
+ pages: 0,
+ limit: 0,
+ },
+ searchParams: '',
+ pagination: { next: { page: 1 } },
+ filters: {
+ series: '',
+ playStatus: '',
+ genre: '',
+ os: '',
+ store: '',
+ controller: '',
+ developer: '',
+ publisher: '',
+ soundtrack: '',
+ intel: '',
+ wine: '',
+ rating: '',
+ steamRating: '',
+ },
+ data: [defaultGame as Data]
+} as GameList)
+
+export const detailedGameState = hookstate({
+ success: false,
+ data: defaultGame as Data
+} as Game)
diff --git a/src/stateManagement/loadingState.ts b/src/stateManagement/loadingState.ts
new file mode 100644
index 0000000..f712077
--- /dev/null
+++ b/src/stateManagement/loadingState.ts
@@ -0,0 +1,15 @@
+import {hookstate, SetStateAction} from '@hookstate/core'
+
+export const loadingState = hookstate(false)
+export const loadingAndSaveState = hookstate(false)
+export const loadingAndContinueState = hookstate(false)
+export const openAndCloseDeleteDialogue = hookstate(false)
+export const openAndCloseSearchDialogue = hookstate(false)
+export const showFiltersModuleState = hookstate(false)
+export const searchCompletedState = hookstate(false)
+
+export const changeLoadState = (bool: SetStateAction) => {
+ loadingState.set(bool)
+ loadingAndContinueState.set(bool)
+ loadingAndSaveState.set(bool)
+}
\ No newline at end of file
diff --git a/src/stateManagement/ratingState.ts b/src/stateManagement/ratingState.ts
new file mode 100644
index 0000000..7ac2d24
--- /dev/null
+++ b/src/stateManagement/ratingState.ts
@@ -0,0 +1,4 @@
+import { hookstate } from '@hookstate/core'
+
+export const ratingState = hookstate('[]')
+export const steamRatingState = hookstate('[]')
diff --git a/src/stateManagement/tagState.ts b/src/stateManagement/tagState.ts
new file mode 100644
index 0000000..9a01697
--- /dev/null
+++ b/src/stateManagement/tagState.ts
@@ -0,0 +1,30 @@
+import { hookstate } from '@hookstate/core'
+import { genreTags } from '../models/tags'
+
+export const tagState = hookstate({
+ genres: genreTags,
+ series: [
+ {
+ value: '',
+ label: '',
+ },
+ ],
+ store: [
+ {
+ value: '',
+ label: '',
+ },
+ ],
+ developer: [
+ {
+ value: '',
+ label: '',
+ },
+ ],
+ publisher: [
+ {
+ value: '',
+ label: '',
+ },
+ ],
+})
diff --git a/src/stateManagement/userState.ts b/src/stateManagement/userState.ts
new file mode 100644
index 0000000..fea4625
--- /dev/null
+++ b/src/stateManagement/userState.ts
@@ -0,0 +1,103 @@
+import defaultUser from '../models/defaultUser'
+import User, {UserFormValues, UserPreferences} from '../models/user'
+import {hookstate, useHookstate} from '@hookstate/core'
+import {localstored} from '@hookstate/localstored'
+import agent from '../api/agent'
+import {history} from '..'
+
+export const loggedInUser = hookstate(defaultUser as User)
+export const userToken = hookstate('')
+export const loggedInState = hookstate(false)
+export const appLoadedState = hookstate(false)
+export const adminMode = hookstate(false)
+export const loadedPreferences = hookstate({} as UserPreferences)
+export const userPreferences = hookstate([] as UserPreferences[],
+ localstored({
+ key: 'userPreferences'
+ }))
+
+export const createUser = async (creds: UserFormValues) => {
+ try {
+ const user = await agent.Account.register(creds)
+ setToken(user.token)
+ await getUser(user.token)
+ loggedInState.set(true)
+ history.push('/games')
+ } catch (error) {
+ throw error
+ }
+}
+
+export const login = async (creds: UserFormValues) => {
+ try {
+ const user = await agent.Account.login(creds)
+ setToken(user.token)
+ await getUser(user.token)
+ loggedInState.set(true)
+ history.push('/games')
+ } catch (error) {
+ throw error
+ }
+}
+
+export const logout = () => {
+ adminMode.set(false)
+ setToken('')
+ window.localStorage.removeItem('jwt')
+ loggedInUser.set(defaultUser)
+ loggedInState.set(false)
+ history.push('/')
+}
+
+export const setToken = (token: string) => {
+ if (token.length > 1) window.localStorage.setItem('jwt', token)
+ userToken.set(token)
+}
+
+export const adminModeToggle = () => {
+ adminMode.set(toggle => !toggle)
+ const toggle = adminMode.get()
+ if (toggle) {
+ window.localStorage.setItem('admin', 'active')
+ } else {
+ window.localStorage.removeItem('admin')
+ }
+}
+
+export const getUser = async (token: string) => {
+ try {
+ userToken.set(token)
+ const user = await agent.Account.current()
+ loggedInState.set(true)
+ loggedInUser.set(user.data)
+ const userPreferencesExist = userPreferences.get().findIndex(x => x.id == user.data._id)
+ if(userPreferencesExist === -1) {
+ userPreferences.merge([{
+ id: user.data._id,
+ theme: 'light',
+ stickyNav: true,
+ }])
+ }
+ const currentPreferences = userPreferences.get().filter(x => x.id == user.data._id)
+ loadedPreferences.set({
+ id: currentPreferences[0].id,
+ theme: currentPreferences[0].theme,
+ stickyNav: currentPreferences[0].stickyNav
+ })
+ } catch (error) {
+ console.error(error)
+ }
+}
+
+export const updateUserPreferences = async (pref: UserPreferences) => {
+ try {
+ const user = await agent.Account.current()
+ const foundIndex = userPreferences.get().findIndex(x => x.id === user.data._id)
+ userPreferences[foundIndex].set(pref)
+ loadedPreferences.set(pref)
+ } catch (err) {
+ console.error(err)
+ }
+}
+
+export const setAppLoaded = (bool: boolean) => appLoadedState.set(bool)
diff --git a/src/styles/overrides.css b/src/styles/overrides.css
new file mode 100644
index 0000000..5ad1402
--- /dev/null
+++ b/src/styles/overrides.css
@@ -0,0 +1,91 @@
+.bb_ul {
+ padding-left: 2rem;
+}
+
+.empty-icons {
+ display: flex !important;
+}
+.filled-icons {
+ /*noinspection CssInvalidPropertyValue*/
+ display: -webkit-inline-box !important;
+}
+
+.jodit_theme_chakra {
+ --jd-color-background-default: #1A202C;
+ --jd-color-border: #474025;
+ --jd-color-panel: #5fd3a2;
+ --jd-color-icon: #8b572a;
+}
+
+.jodit-toolbar__box:not(:empty):not(:empty) {
+ background: #1A202C;
+}
+
+.jodit_theme_dark .jodit-workplace + .jodit-status-bar:not(:empty) {
+ background: #1A202C;
+}
+
+.jodit_theme_dark .jodit-wysiwyg {
+ background: #1A202C;
+}
+
+.jodit_theme_dark .jodit-popup__content {
+ background: #1A202C;
+}
+
+.jodit_theme_dark .jodit-toolbar-button:hover:not([disabled]) {
+ background-color: #4A5568;
+}
+
+.jodit_theme_dark .jodit-toolbar-button__button:hover:not([disabled]) {
+ background-color: #4A5568;
+}
+
+.jodit-toolbar-button__button:active:not([disabled]) {
+ background-color: #2D3748;
+}
+
+.jodit-toolbar-button__button:focus-visible:not([disabled]) {
+ background-color: #718096;
+}
+
+.jodit_theme_dark .jodit-toolbar-button__trigger:hover:not([disabled]) {
+ background-color: #4A5568;
+}
+
+.jodit-toolbar-button__trigger:active:not([disabled]) {
+ background-color: #2D3748;
+}
+
+.jodit-toolbar-button__button[aria-pressed="true"]:not([disabled]) {
+ background-color: #718096;
+}
+
+.jodit-source {
+ background-color: #2D3748;
+}
+
+.ace-idle-fingers {
+ background-color: #2D3748;
+}
+
+.ace-idle-fingers .ace_gutter {
+ background-color: #2D3748;
+}
+
+.ace-idle-fingers .ace_gutter-active-line {
+ background-color: #4A5568;
+}
+
+.jodit-dialog_theme_dark .jodit-dialog__panel {
+ background-color: #4A5568;
+ border-radius: 5px;
+}
+
+.jodit-ui-button_variant_primary {
+ background-color: #2D3748;
+}
+
+.jodit-dialog_theme_dark .jodit-ui-button:hover:not([disabled]) {
+ background-color: #718096;
+}
\ No newline at end of file
diff --git a/src/styles/theme.tsx b/src/styles/theme.tsx
new file mode 100644
index 0000000..0ea0604
--- /dev/null
+++ b/src/styles/theme.tsx
@@ -0,0 +1,18 @@
+import { extendTheme, Theme } from '@chakra-ui/react'
+import { mode } from '@chakra-ui/theme-tools'
+import {loadedPreferences} from "../stateManagement/userState";
+
+
+
+export default extendTheme({
+ initialColorMode: loadedPreferences.theme,
+ useSystemColorMode: false,
+ styles: {
+ global: (props: Theme) => ({
+ body: {
+ bg: mode('gray.200', 'gray.800')(props),
+ },
+ })
+ },
+})
+
\ No newline at end of file
diff --git a/src/styles/typography.css b/src/styles/typography.css
new file mode 100644
index 0000000..df19545
--- /dev/null
+++ b/src/styles/typography.css
@@ -0,0 +1,6 @@
+@font-face {
+ font-family: "RetroGaming";
+ src: url("../resources/fonts/retrogaming.ttf");
+ font-weight: 400;
+ font-style: normal;
+}
diff --git a/src/styles/videoBackground.css b/src/styles/videoBackground.css
new file mode 100644
index 0000000..4a6d83e
--- /dev/null
+++ b/src/styles/videoBackground.css
@@ -0,0 +1,11 @@
+.videoBackground {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100%;
+ z-index: -2;
+ overflow: hidden;
+ object-fit: cover;
+ display: block;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..605dbbd
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "types": ["vite/client"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": false,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noFallthroughCasesInSwitch": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..b1b5f91
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()]
+})