moved frontend from monorepo
This commit is contained in:
parent
0e84e37a74
commit
3af14dee95
9
config-overrides.js
Normal file
9
config-overrides.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = function override(webpackConfig) {
|
||||
webpackConfig.module.rules.push({
|
||||
test: /\.mjs$/,
|
||||
include: /node_modules/,
|
||||
type: 'javascript/auto',
|
||||
})
|
||||
|
||||
return webpackConfig
|
||||
}
|
47
index.html
Normal file
47
index.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<!--suppress ALL -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<!--
|
||||
Notice the use of in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", /favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Games Database</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
3
notes.txt
Normal file
3
notes.txt
Normal file
@ -0,0 +1,3 @@
|
||||
Include somewhere with image on error page. Probably in video alt.
|
||||
|
||||
Image by <a href="https://pixabay.com/users/alexantropov86-2691829/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2213140">Alexander Antropov</a> from <a href="https://pixabay.com/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2213140">Pixabay</a>
|
76
package.json
Normal file
76
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
51
public/assets/Logo.svg
Normal file
51
public/assets/Logo.svg
Normal file
@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 998 185" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-431,-466)">
|
||||
<g id="Logo">
|
||||
<g transform="matrix(0.239118,0,0,0.239118,507.822,474.94)">
|
||||
<g transform="matrix(1,0,0,1,-99.5515,0)">
|
||||
<path d="M238.034,381.504L308.822,381.504L308.822,431L204.975,431L155.479,381.504L155.479,232.644L204.975,183.149L238.034,183.149L238.034,381.504ZM281.74,293.533L395.112,293.533L395.112,431L321.896,431L321.896,342.655L281.74,342.655L281.74,293.533ZM251.108,232.644L251.108,183.149L351.781,183.149L395.112,232.644L395.112,257.485L312.558,257.485L312.558,232.644L251.108,232.644Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<path d="M563.397,431L563.397,282.14L445.915,282.14L445.915,257.485L470.757,232.644L594.589,232.644L644.085,282.14L644.085,431L563.397,431ZM550.323,315.573L550.323,349.939L507.738,349.939L507.738,384.68L550.323,384.68L550.323,406.532L525.669,431L462.539,431L429.292,398.314L429.292,348.632L462.539,315.573L550.323,315.573Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M684.055,431L684.055,232.644L766.796,232.644L766.796,431L684.055,431ZM813.49,282.14L779.87,282.14L779.87,258.793L805.832,232.644L869.71,232.644L896.232,259.166L896.232,431L813.49,431L813.49,282.14ZM942.926,282.14L909.306,282.14L909.306,259.166L934.708,232.644L985.137,232.644L1025.48,273.361L1025.48,431L942.926,431L942.926,282.14Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M1143.71,431L1110.65,431L1061.15,381.504L1061.15,282.14L1110.65,232.644L1143.71,232.644L1143.71,431ZM1156.78,351.434L1156.78,316.88L1200.3,316.88L1200.3,282.14L1156.78,282.14L1156.78,232.644L1226.83,232.644L1276.32,282.14L1276.32,351.434L1156.78,351.434ZM1156.78,431L1156.78,384.68L1267.73,384.68L1267.73,406.159L1243.07,431L1156.78,431Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M1354.58,232.644L1387.64,232.644L1387.64,302.685L1481.77,302.685L1524.73,346.017L1524.73,384.68L1478.6,431L1442.18,431L1442.18,356.103L1352.9,356.103L1309.94,314.825L1309.94,277.097L1354.58,232.644ZM1392.5,394.579L1429.1,394.579L1429.1,431L1344.12,431L1309.75,390.096L1309.75,374.22L1392.5,374.22L1392.5,394.579ZM1442.18,269.065L1400.71,269.065L1400.71,232.644L1488.5,232.644L1518.01,262.155L1518.01,284.568L1442.18,284.568L1442.18,269.065Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M1754.09,431L1754.09,381.504L1815.36,381.504L1815.36,232.644L1754.09,232.644L1754.09,183.149L1848.41,183.149L1898.1,232.644L1898.1,381.504L1848.41,431L1754.09,431ZM1658.46,183.149L1741.02,183.149L1741.02,431L1658.46,431L1658.46,183.149Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M2064.33,431L2064.33,282.14L1946.85,282.14L1946.85,257.485L1971.69,232.644L2095.52,232.644L2145.01,282.14L2145.01,431L2064.33,431ZM2051.25,315.573L2051.25,349.939L2008.67,349.939L2008.67,384.68L2051.25,384.68L2051.25,406.532L2026.6,431L1963.47,431L1930.22,398.314L1930.22,348.632L1963.47,315.573L2051.25,315.573Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M2266.98,381.504L2321.52,381.504L2321.52,431L2234.11,431L2184.42,381.504L2184.42,282.14L2167.99,282.14L2167.99,232.644L2184.42,232.644L2184.42,211.165L2266.98,183.149L2266.98,381.504ZM2280.05,282.14L2280.05,232.644L2324.51,232.644L2324.51,282.14L2280.05,282.14Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M2483.83,431L2483.83,282.14L2366.34,282.14L2366.34,257.485L2391.18,232.644L2515.02,232.644L2564.51,282.14L2564.51,431L2483.83,431ZM2470.75,315.573L2470.75,349.939L2428.17,349.939L2428.17,384.68L2470.75,384.68L2470.75,406.532L2446.1,431L2382.97,431L2349.72,398.314L2349.72,348.632L2382.97,315.573L2470.75,315.573Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M2684.24,157L2684.24,431L2601.68,431L2601.68,157L2684.24,157ZM2734.29,282.14L2697.31,282.14L2697.31,257.112L2721.59,232.644L2766.79,232.644L2816.47,282.14L2816.47,381.504L2766.79,431L2697.31,431L2697.31,384.68L2734.29,384.68L2734.29,282.14Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M2982.7,431L2982.7,282.14L2865.22,282.14L2865.22,257.485L2890.06,232.644L3013.89,232.644L3063.39,282.14L3063.39,431L2982.7,431ZM2969.63,315.573L2969.63,349.939L2927.04,349.939L2927.04,384.68L2969.63,384.68L2969.63,406.532L2944.97,431L2881.85,431L2848.6,398.314L2848.6,348.632L2881.85,315.573L2969.63,315.573Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M3143.7,232.644L3176.76,232.644L3176.76,302.685L3270.9,302.685L3313.86,346.017L3313.86,384.68L3267.72,431L3231.3,431L3231.3,356.103L3142.02,356.103L3099.07,314.825L3099.07,277.097L3143.7,232.644ZM3181.62,394.579L3218.23,394.579L3218.23,431L3133.24,431L3098.88,390.096L3098.88,374.22L3181.62,374.22L3181.62,394.579ZM3231.3,269.065L3189.84,269.065L3189.84,232.644L3277.62,232.644L3307.13,262.155L3307.13,284.568L3231.3,284.568L3231.3,269.065Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,292.742,0)">
|
||||
<path d="M3427.04,431L3393.98,431L3344.49,381.504L3344.49,282.14L3393.98,232.644L3427.04,232.644L3427.04,431ZM3440.12,351.434L3440.12,316.88L3483.64,316.88L3483.64,282.14L3440.12,282.14L3440.12,232.644L3510.16,232.644L3559.65,282.14L3559.65,351.434L3440.12,351.434ZM3440.12,431L3440.12,384.68L3551.06,384.68L3551.06,406.159L3526.41,431L3440.12,431Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="gamepad.svg" transform="matrix(0.127966,0,0,0.127966,923.759,545.241)">
|
||||
<g transform="matrix(1,0,0,1,-256,-256)">
|
||||
<path d="M155.084,125.945C154.624,125.945 154.158,125.955 153.687,125.979C148.041,126.264 141.59,128.443 132.98,134.183C111.156,148.733 81.068,194.578 65.146,244.188C49.226,293.798 47.1,346.438 71.082,377.154C75.224,382.46 84.469,386.084 94.838,386C105.054,385.916 115.52,382.162 121.32,376.56C122.342,375.09 130.616,363.224 142.71,349.156C155.573,334.196 171.426,317.47 188.545,310.379C230.408,293.039 281.569,293.039 323.432,310.379C340.55,317.471 356.402,334.197 369.266,349.157C381.361,363.225 389.636,375.09 390.656,376.561C396.456,382.163 406.923,385.917 417.139,386.001C427.507,386.086 436.751,382.461 440.894,377.155C464.867,346.451 462.779,293.58 446.872,243.868C430.965,194.155 400.818,148.342 379.089,134.244C367.591,126.784 359.891,125.514 352.804,126.604C345.716,127.697 338.457,131.801 329.938,137.674C312.9,149.42 291.04,167.694 255.986,167.694C220.774,167.694 198.871,149.18 181.856,137.338C173.351,131.418 166.126,127.313 159.113,126.26C157.798,126.062 156.463,125.948 155.083,125.943L155.084,125.945ZM367.988,174.695C376.765,174.695 383.988,181.918 383.988,190.695C383.988,199.472 376.765,206.695 367.988,206.695C359.211,206.695 351.988,199.472 351.988,190.695C351.988,181.918 359.211,174.695 367.988,174.695ZM135,183L153,183L153,215L185,215L185,233L153,233L153,265L135,265L135,233L103,233L103,215L135,215L135,183ZM335.988,206.695C344.765,206.695 351.988,213.918 351.988,222.695C351.988,231.472 344.765,238.695 335.988,238.695C327.211,238.695 319.988,231.472 319.988,222.695C319.988,213.918 327.211,206.695 335.988,206.695ZM399.988,206.695C408.765,206.695 415.988,213.918 415.988,222.695C415.988,231.472 408.765,238.695 399.988,238.695C391.211,238.695 383.988,231.472 383.988,222.695C383.988,213.918 391.211,206.695 399.988,206.695ZM367.988,238.695C376.765,238.695 383.988,245.918 383.988,254.695C383.988,263.472 376.765,270.695 367.988,270.695C359.211,270.695 351.988,263.472 351.988,254.695C351.988,245.918 359.211,238.695 367.988,238.695ZM207.988,245.695L239.988,245.695L239.988,263.695L207.988,263.695L207.988,245.695ZM271.988,245.695L299.885,245.695L299.885,263.695L271.988,263.695L271.988,245.695Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="pc.svg" transform="matrix(0.359737,0,0,0.359737,523.093,558.093)">
|
||||
<g transform="matrix(1,0,0,1,-256,-256)">
|
||||
<path d="M29.65,117.89L29.65,394.11L154.27,394.11L154.27,117.89L29.65,117.89ZM120.2,371.05C114.166,371.05 109.2,366.084 109.2,360.05C109.2,354.016 114.166,349.05 120.2,349.05C126.234,349.05 131.2,354.016 131.2,360.05C131.2,366.084 126.234,371.05 120.2,371.05ZM138.2,181.89L45.56,181.89L45.56,165.89L138.19,165.89L138.19,181.89L138.2,181.89ZM138.2,149.89L45.56,149.89L45.56,133.89L138.19,133.89L138.19,149.89L138.2,149.89ZM291.2,338.4L364.3,338.4L364.3,378.11L406.04,378.11L406.04,394.11L249.48,394.11L249.48,378.11L291.22,378.11L291.22,338.4L291.2,338.4ZM173.2,117.89L173.2,322.4L482.35,322.4L482.35,117.89L173.19,117.89L173.2,117.89ZM466.35,306.4L189.19,306.4L189.19,133.89L466.35,133.89L466.35,306.4Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 9.6 KiB |
34
public/assets/SteamAndDeckLogo.svg
Normal file
34
public/assets/SteamAndDeckLogo.svg
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1080 1080" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path id="Artboard1" d="M1080,216C1080,96.786 983.214,0 864,0L216,0C96.786,0 0,96.786 0,216L0,864C0,983.214 96.786,1080 216,1080L864,1080C983.214,1080 1080,983.214 1080,864L1080,216Z" style="fill:url(#_Radial1);"/>
|
||||
<clipPath id="_clip2">
|
||||
<path id="Artboard11" serif:id="Artboard1" d="M1080,216C1080,96.786 983.214,0 864,0L216,0C96.786,0 0,96.786 0,216L0,864C0,983.214 96.786,1080 216,1080L864,1080C983.214,1080 1080,983.214 1080,864L1080,216Z"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#_clip2)">
|
||||
<g transform="matrix(0.690209,0.72361,-0.72361,0.690209,558.037,-223.462)">
|
||||
<path d="M326.9,728.816C280.975,678.813 253,612.591 253,540C253,384.36 381.6,258 540,258C698.4,258 827,384.36 827,540C827,695.64 698.4,822 540,822C489.157,822 441.384,808.982 399.949,786.155L550.64,724.438L520.037,649.715L326.9,728.816Z" style="fill:url(#_Linear3);"/>
|
||||
</g>
|
||||
<path d="M541.381,95.002C786.349,95.748 985,294.858 985,540C985,785.142 786.349,984.252 541.381,984.998L541.381,885.702C731.547,884.957 885.705,730.339 885.705,540C885.705,349.661 731.547,195.043 541.381,194.298L541.381,95.002Z"/>
|
||||
<g transform="matrix(0.737705,0,0,0.737705,104.705,102.639)">
|
||||
<circle cx="708" cy="479" r="61" style="fill:rgb(31,62,107);"/>
|
||||
</g>
|
||||
<path d="M627,363C678.328,363 720,404.672 720,456C720,507.328 678.328,549 627,549C575.672,549 534,507.328 534,456C534,404.672 575.672,363 627,363ZM627,397.428C659.327,397.428 685.572,423.673 685.572,456C685.572,488.327 659.327,514.572 627,514.572C594.673,514.572 568.428,488.327 568.428,456C568.428,423.673 594.673,397.428 627,397.428Z" style="fill:rgb(31,62,107);"/>
|
||||
<g transform="matrix(-0.486753,-0.597406,0.775249,-0.631656,372.779,1129.97)">
|
||||
<path d="M534,564L376,564L415.5,406L494.5,406L534,564Z" style="fill:rgb(31,62,107);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,19.8252,13)">
|
||||
<path d="M406,553.837C444.127,553.837 475.081,584.792 475.081,622.919C475.081,661.046 444.127,692 406,692C367.873,692 336.919,661.046 336.919,622.919C336.919,584.792 367.873,553.837 406,553.837ZM406,571.108C434.595,571.108 457.811,594.323 457.811,622.919C457.811,651.514 434.595,674.73 406,674.73C377.405,674.73 354.189,651.514 354.189,622.919C354.189,594.323 377.405,571.108 406,571.108Z" style="fill:rgb(31,62,107);"/>
|
||||
</g>
|
||||
<g transform="matrix(1.31489,0.587666,-0.822016,1.83925,393.401,-726.359)">
|
||||
<path d="M296.936,630.919C283.279,618.809 272.542,605.247 265.089,590.837L410,590.837L410,630.919L296.936,630.919Z" style="fill:rgb(31,62,107);"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,15.8252,5)">
|
||||
<circle cx="410" cy="630.919" r="40.081" style="fill:rgb(31,62,107);"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="_Radial1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(540,0,0,540,540,540)"><stop offset="0" style="stop-color:rgb(42,145,215);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(7,16,39);stop-opacity:1"/></radialGradient>
|
||||
<linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(574,0,0,564,253,540)"><stop offset="0" style="stop-color:rgb(183,94,206);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(47,148,242);stop-opacity:0.7"/><stop offset="1" style="stop-color:rgb(0,166,255);stop-opacity:0.6"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@ -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"
|
||||
}
|
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.js.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
3
src/@types/game.ts
Normal file
3
src/@types/game.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: RecursivePartial<T[P]>;
|
||||
};
|
9
src/App.test.tsx
Normal file
9
src/App.test.tsx
Normal file
@ -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(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
112
src/App.tsx
Normal file
112
src/App.tsx
Normal file
@ -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<Location>()
|
||||
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) <LoadingModal />
|
||||
|
||||
useEffect(() => {
|
||||
if(loadedPreferences.get().theme !== colorMode) {
|
||||
localStorage.removeItem('chakra-ui-color-mode')
|
||||
toggleColorMode()
|
||||
}
|
||||
}, [loadedPreferences.get().theme]);
|
||||
|
||||
// noinspection SpellCheckingInspection
|
||||
return (
|
||||
<>
|
||||
<ToastContainer position='bottom-right' hideProgressBar />
|
||||
{loggedIn ? <Header /> : ''}
|
||||
<Route exact path='/'>
|
||||
{loggedIn ? <Redirect to='/games' /> : <Home />}
|
||||
</Route>
|
||||
<Box mt={loadedPreferences.get().stickyNav ? "20" : "4"} mb={'4'}>
|
||||
{/*<Box mt={loadedPreferences.get().stickyNav ? isSmallerThan768 ? "15%" : "9%" : "4"} mb={'4'}>*/}
|
||||
<Switch>
|
||||
<Route
|
||||
key={location.key}
|
||||
path={['/games/create', '/games/:id/edit']}
|
||||
component={() => <GameForm />}
|
||||
/>
|
||||
<Route path='/games/:id'>
|
||||
<GameDetails />
|
||||
</Route>
|
||||
<Route path='/games'>
|
||||
<GameDashboard />
|
||||
</Route>
|
||||
<Route path='/nogames'>
|
||||
<NoGames />
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginForm />
|
||||
</Route>
|
||||
<Route path='/profile'>
|
||||
<UserProfile />
|
||||
</Route>
|
||||
<Route path='/forgotpassword'>
|
||||
<ForgotPassword />
|
||||
</Route>
|
||||
<Route path='/resetpassword/:token'>
|
||||
<ResetPassword />
|
||||
</Route>
|
||||
<Route path='/create-user'>
|
||||
<CreateUser />
|
||||
</Route>
|
||||
<Route path='/edit-user'>
|
||||
<EditUser />
|
||||
</Route>
|
||||
<Route path='/errors'>
|
||||
<TestErrors />
|
||||
</Route>
|
||||
<Route path='/something-went-wrong'>
|
||||
<SomethingWentWrong />
|
||||
</Route>
|
||||
<Route>
|
||||
<NotFound />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Box>
|
||||
{loggedIn ? <Footer /> : ''}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
128
src/api/agent.ts
Normal file
128
src/api/agent.ts
Normal file
@ -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 = <T>(response: AxiosResponse<T>) => response.data
|
||||
|
||||
interface Tags {
|
||||
success: boolean,
|
||||
data: {
|
||||
genre: string[]
|
||||
series: string[]
|
||||
store: string[]
|
||||
developer: string[]
|
||||
publisher: string[]
|
||||
}
|
||||
}
|
||||
|
||||
const requests = {
|
||||
get: <T>(url: string) => axios.get<T>(url).then(responseBody),
|
||||
post: <T>(url: string, body: {}) =>
|
||||
axios.post<T>(url, body).then(responseBody),
|
||||
put: <T>(url: string, body: {}) => axios.put<T>(url, body).then(responseBody),
|
||||
delete: <T>(url: string) => axios.delete<T>(url).then(responseBody),
|
||||
}
|
||||
|
||||
const Games = {
|
||||
list: (filter: string) => requests.get<GameList>(`/games?${filter}`),
|
||||
details: (id: string) => requests.get<Game>(`/games/${id}`),
|
||||
create: (game: Data) => axios.post<void>('/games', game),
|
||||
update: (id: string, game: Data) => axios.put<void>(`/games/${id}`, game),
|
||||
playStatus: (id: string, playStatus: RecursivePartial<Data>) => axios.put<void>(`/games/${id}`, playStatus),
|
||||
rating: (id: string, rating: RecursivePartial<Data>) => axios.put<void>(`/games/${id}`, rating),
|
||||
delete: (id: string) => axios.delete<void>(`/games/${id}`),
|
||||
}
|
||||
|
||||
const GamesAdmin = {
|
||||
list: (filter: string) => requests.get<GameList>(`/admin/games?${filter}`),
|
||||
details: (id: string) => requests.get<Game>(`/admin/games/${id}`),
|
||||
create: (game: Data) => axios.post<void>('/admin/games', game),
|
||||
update: (id: string, game: Data) => axios.put<void>(`/admin/games/${id}`, game),
|
||||
delete: (id: string) => axios.delete<void>(`/admin/games/${id}`),
|
||||
}
|
||||
|
||||
const RetrieveTags = {
|
||||
tags: () => requests.get<Tags>('/tags'),
|
||||
genres: () => requests.get<string[]>('/tags/genres'),
|
||||
series: () => requests.get<string[]>('/tags/series'),
|
||||
store: () => requests.get<string[]>('/tags/store'),
|
||||
developer: () => requests.get<string[]>('/tags/developer'),
|
||||
publisher: () => requests.get<string[]>('/tags/publisher'),
|
||||
}
|
||||
|
||||
const Account = {
|
||||
current: () => requests.get<UserAPI>('/auth/me'),
|
||||
login: (user: UserFormValues) => requests.post<User>('/auth/login', user),
|
||||
register: (user: UserFormValues) => requests.post<User>('/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
|
40
src/componentUtils/SelectValues.ts
Normal file
40
src/componentUtils/SelectValues.ts
Normal file
@ -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'])
|
25
src/componentUtils/changeActiveFilterHeader.ts
Normal file
25
src/componentUtils/changeActiveFilterHeader.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import {State} from "@hookstate/core";
|
||||
import {Filters} from "../models/game";
|
||||
|
||||
export const ChangeActiveFilterHeader = (
|
||||
filters: Filters,
|
||||
searchParams: string,
|
||||
setShowFiltersModuleState: State<boolean>
|
||||
) => {
|
||||
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)
|
||||
}
|
39
src/componentUtils/getWithFilters.ts
Normal file
39
src/componentUtils/getWithFilters.ts
Normal file
@ -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<GameList>,
|
||||
loadedState: State<boolean>,
|
||||
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
|
18
src/componentUtils/retrieveTags.ts
Normal file
18
src/componentUtils/retrieveTags.ts
Normal file
@ -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)
|
||||
}
|
8
src/componentUtils/stringArrayToSelectObject.ts
Normal file
8
src/componentUtils/stringArrayToSelectObject.ts
Normal file
@ -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;
|
132
src/components/ActiveFilters.tsx
Normal file
132
src/components/ActiveFilters.tsx
Normal file
@ -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 ? (
|
||||
<Stack my={5} alignItems={{
|
||||
base: 'start',
|
||||
md: 'center'
|
||||
}} direction={{
|
||||
base: 'column',
|
||||
md: 'row'
|
||||
}} spacing={4}>
|
||||
{searchParams.length >= 1 && (
|
||||
<>
|
||||
<h2>Search: </h2>
|
||||
<Tag
|
||||
size={'md'}
|
||||
borderRadius="full"
|
||||
variant="solid"
|
||||
colorScheme="blue"
|
||||
>
|
||||
<TagLabel><Link
|
||||
onMouseDown={() => setSearchModalOpen.set(true)}>{searchParams}</Link></TagLabel>
|
||||
<TagCloseButton onMouseDown={async () => {
|
||||
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,
|
||||
)
|
||||
}} />
|
||||
</Tag>
|
||||
</>
|
||||
)}
|
||||
<Stack alignItems={{
|
||||
base: 'start',
|
||||
md: 'center'
|
||||
}} direction={{
|
||||
base: 'column',
|
||||
md: 'row'
|
||||
}} >
|
||||
<h2>Active Filters: </h2>
|
||||
{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 && (
|
||||
<>
|
||||
<HStack key={num}>
|
||||
<h3 style={{ textTransform: 'capitalize' }}>{filter}</h3>
|
||||
<HStack spacing={4}>
|
||||
{filters[filter as keyof typeof filters].split(',').map((item: string, index) => (
|
||||
<Tag
|
||||
size={'md'}
|
||||
key={index}
|
||||
borderRadius="full"
|
||||
variant="solid"
|
||||
colorScheme="blue"
|
||||
>
|
||||
<TagLabel>{
|
||||
item.includes('lte')
|
||||
? item.replace('lte', '<= ')
|
||||
: item.includes('gte') ? item.replace('gte', '>= ')
|
||||
: item}
|
||||
</TagLabel>
|
||||
<TagCloseButton onMouseDown={() => removeFilters(filter, item)} />
|
||||
</Tag>
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<Spacer />
|
||||
<ResetFilterButton />
|
||||
</Stack>
|
||||
) : <></>
|
||||
}
|
||||
|
||||
export default ActiveFilters
|
77
src/components/DeleteDialogue.tsx
Normal file
77
src/components/DeleteDialogue.tsx
Normal file
@ -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 (
|
||||
<AlertDialog isOpen={open} leastDestructiveRef={cancelRef} onClose={handleClose}>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
|
||||
<AlertDialogHeader fontSize='lg' fontWeight='bold'>Delete?</AlertDialogHeader>
|
||||
<AlertDialogBody>
|
||||
Are you sure you would like to delete <strong>{game.title}</strong>?
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onMouseDown={handleClose}>
|
||||
<Icon name='remove' mr='1' /> <Text mt='1'>No</Text>
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme='red'
|
||||
onMouseDown={() => {
|
||||
deleteGame(id)
|
||||
}}
|
||||
ml={3}
|
||||
autoFocus
|
||||
>
|
||||
<Icon name='checkmark' mr='1' /> <Text mt='1'>Yes</Text>
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
350
src/components/FilterBar.tsx
Normal file
350
src/components/FilterBar.tsx
Normal file
@ -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<null>
|
||||
filterLoadingState: StateMethods<boolean, any>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Drawer
|
||||
isOpen={filterIsOpen}
|
||||
placement="right"
|
||||
onClose={filterOnClose}
|
||||
finalFocusRef={btnRef}
|
||||
>
|
||||
<DrawerOverlay/>
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton/>
|
||||
<DrawerHeader>
|
||||
<Heading as={'h3'}>Filters</Heading>
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
<AdminMode/>
|
||||
<Divider my={'3'}/>
|
||||
<Center>
|
||||
<ResetFilterButton/>
|
||||
</Center>
|
||||
<Divider my={'3'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Series</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("series")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="series"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("series", value)
|
||||
}}
|
||||
options={tags.series}
|
||||
/>
|
||||
<Divider my={'3'}/>
|
||||
<Flex mb={'1'}>
|
||||
<Text mb={'1'} fontSize={'xl'}>Rating</Text>
|
||||
<Spacer />
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={ratingToSelectDefaultValue(useRatingState)}
|
||||
name="rating"
|
||||
onChange={(event) => {
|
||||
if (event === null) return
|
||||
setRatingState.set(event.value)
|
||||
}}
|
||||
options={ratingStateValues}
|
||||
chakraStyles={{
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: "75px"
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("rating")}
|
||||
isClearable
|
||||
name="rating"
|
||||
onChange={(event) => {
|
||||
if(event === null) return changeFilters("rating", "")
|
||||
changeFilters("rating", event.value)
|
||||
}}
|
||||
options={ratingValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Flex mb={'1'}>
|
||||
<Text mb={'1'} fontSize={'xl'}>Steam Rating</Text>
|
||||
<Spacer />
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={ratingToSelectDefaultValue(useSteamRatingState)}
|
||||
name="steamRating"
|
||||
onChange={(event) => {
|
||||
if (event === null) return
|
||||
setSteamRatingState.set(event.value)
|
||||
}}
|
||||
options={ratingStateValues}
|
||||
chakraStyles={{
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: "75px"
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("steamRating")}
|
||||
isClearable
|
||||
name="steamRating"
|
||||
onChange={(event) => {
|
||||
if(event === null) return changeFilters("steamRating", "")
|
||||
changeFilters("steamRating", event.value)
|
||||
}}
|
||||
options={ratingValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Play Status</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("playStatus")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="playStatus"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("playStatus", value)
|
||||
}}
|
||||
options={playStatusValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Genres</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("genre")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="genre"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("genre", value)
|
||||
}}
|
||||
options={tags.genres}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Operating System</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("os")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="os"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("os", value)
|
||||
}}
|
||||
options={osValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Store</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("store")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="store"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("store", value)
|
||||
}}
|
||||
options={tags.store}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Controller Support</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("controller")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="controller"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("controller", value)
|
||||
}}
|
||||
options={controllerValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Developer</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("developer")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="developer"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("developer", value)
|
||||
}}
|
||||
options={tags.developer}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Publisher</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("publisher")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="publisher"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("publisher", value)
|
||||
}}
|
||||
options={tags.publisher}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Do you own the Soundtrack?</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("soundtrack")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="soundtrack"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("soundtrack", value)
|
||||
}}
|
||||
options={yesNo}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Does in work on Intel?</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("intel")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="intel"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("intel", value)
|
||||
}}
|
||||
options={wineIntelValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
<Text mb={'1'} fontSize={'xl'}>Does is work with Wine?</Text>
|
||||
<Select
|
||||
size={'sm'}
|
||||
defaultValue={filterToSelectDefaultValue("wine")}
|
||||
isClearable
|
||||
isMulti
|
||||
name="wine"
|
||||
onChange={(event) => {
|
||||
const value = event.map(a => a.value).join(',')
|
||||
changeFilters("wine", value)
|
||||
}}
|
||||
options={wineIntelValues}
|
||||
/>
|
||||
<Divider my={'1'}/>
|
||||
</DrawerBody>
|
||||
|
||||
<DrawerFooter>
|
||||
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterBar
|
17
src/components/Footer.tsx
Normal file
17
src/components/Footer.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { Box, Stack } from "@chakra-ui/react";
|
||||
|
||||
export default function Footer() {
|
||||
// noinspection SpellCheckingInspection
|
||||
return (
|
||||
<Box as='footer' role='contentinfo' mx='auto' maxW='7xl' py='12' px={{ base: '4', md: '8' }}>
|
||||
<Stack>
|
||||
<Stack direction="row" spacing="4" align="center" justify="space-between">
|
||||
|
||||
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
156
src/components/GameDetails.tsx
Normal file
156
src/components/GameDetails.tsx
Normal file
@ -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<Data> = 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 <LoadingModal />
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
return (
|
||||
<>
|
||||
{isSummaryOpen ?
|
||||
<SummaryModal isOpen={isSummaryOpen} onClose={onSummaryClose} game={game} />
|
||||
|
||||
: ''}
|
||||
{isSystemOpen ?
|
||||
<SystemRequirementsModal isOpen={isSystemOpen} onClose={onSystemClose} game={game} />
|
||||
|
||||
: ''}
|
||||
<Container maxW="container.xl" mt="5">
|
||||
<Flex direction={{ base: 'column', md: 'row' }}>
|
||||
<Box p={2} alignSelf={{
|
||||
base: 'center',
|
||||
md: 'start'
|
||||
}}>
|
||||
<Heading as="h1">{game.title}</Heading>
|
||||
</Box>
|
||||
<Spacer />
|
||||
<Stack spacing={[1, 5]} direction={'row'} alignSelf={{
|
||||
base: 'center',
|
||||
md: 'end'
|
||||
}} pb="3">
|
||||
{game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
|
||||
(<ChakraLink href={`steam://run/${game.steamId}`}>
|
||||
<Button variant={'outline'} colorScheme="blue">
|
||||
<Flex>
|
||||
<SteamAndDeckLogo size={'1.5em'} />
|
||||
<Text mt={'1'} ml={'2'}>
|
||||
Open with Steam <ExternalLinkIcon pb={'1'} />
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
</ChakraLink>)}
|
||||
<Button as={Link} to={`/games/${game._id}/edit`} colorScheme="blue">
|
||||
<Text mt={'1'}>
|
||||
Edit
|
||||
</Text>
|
||||
</Button>
|
||||
</Stack>
|
||||
</Flex>
|
||||
<Grid
|
||||
gridTemplateColumns={{
|
||||
base: 'auto',
|
||||
md: '3fr 2fr',
|
||||
}}
|
||||
gridTemplateRows={{
|
||||
base: 'auto',
|
||||
lg: 'auto',
|
||||
}}
|
||||
gridTemplateAreas={{
|
||||
base: `'left' 'right'`,
|
||||
md: `'left right'`,
|
||||
}}
|
||||
gap={3}
|
||||
>
|
||||
<Flex direction="column" gridArea="left">
|
||||
<Box bg={bg} p="5" rounded="md">
|
||||
<ImageSlider game={game} width={'100%'} />
|
||||
</Box>
|
||||
<Box bg={bg} p="5" mt="3" rounded="md">
|
||||
{isSmallerThan768 ?
|
||||
<Flex width="100%">
|
||||
<Button colorScheme='blue' size='lg' width='100%' onMouseDown={onSummaryOpen}>
|
||||
About This Game
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
: <Summary game={game} />
|
||||
}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex direction="column" gridArea="right">
|
||||
<Box bg={bg} p="5" rounded="md">
|
||||
<Features game={game} />
|
||||
</Box>
|
||||
<Box bg={bg} mt="3" p="5" rounded="md">
|
||||
{isSmallerThan768 ?
|
||||
<Flex width="100%">
|
||||
<Button colorScheme='blue' size='lg' width='100%' onMouseDown={onSystemOpen}>
|
||||
System Requirements
|
||||
</Button>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
: <SystemRequirements game={game} />
|
||||
}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GameDetails
|
597
src/components/GameForm.tsx
Normal file
597
src/components/GameForm.tsx
Normal file
@ -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<Data>({
|
||||
_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<HTMLFormElement>) => {
|
||||
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 = <G extends keyof Data>(name: string, value: Data[G]) => {
|
||||
gameState.merge(() => ({[name]: value}))
|
||||
}
|
||||
|
||||
const handleToggle = <G extends keyof Data>(name: G) => {
|
||||
gameState.merge(() => ({[name]: !game[name]}))
|
||||
}
|
||||
|
||||
const handleOSChange = <O extends keyof Os>(name: O) => {
|
||||
gameState.os.merge(() => ({
|
||||
[name]: !game.os[name],
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSystemRequirements = <S extends keyof SystemRequirements,
|
||||
WML extends keyof WindowsOrMacOrLinux>(
|
||||
name: S,
|
||||
minRec: WML,
|
||||
value: string,
|
||||
) => {
|
||||
gameState.systemRequirements[name].merge(() => ({
|
||||
[minRec]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
const handleAccessedBy = <A extends keyof AccessedBy>(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 <LoadingModal/>
|
||||
|
||||
if (open) return <DeleteDialogue
|
||||
id={id}
|
||||
open={open}
|
||||
setOpen={openState.set}
|
||||
setLoading={loadState.set}
|
||||
//@ts-ignore
|
||||
game={game}
|
||||
/>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container maxW="container.md" mt="10">
|
||||
{id ? <Heading as="h1" size="xl">Editing {game.title}</Heading> :
|
||||
<Heading as="h1" size="xl">Add a Game to the Database</Heading>}
|
||||
<form onSubmit={handleSubmit} style={{marginTop: '2rem'}}>
|
||||
<Stack spacing={[1, 5]} direction={{base: 'column', sm: 'row'}} justify={'flex-end'}>
|
||||
<Button
|
||||
isLoading={submitLoading}
|
||||
colorScheme="blue"
|
||||
type="submit"
|
||||
data-name={"save"}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={moveOn}
|
||||
colorScheme="green"
|
||||
type="submit"
|
||||
data-name={"save-and-view"}
|
||||
>
|
||||
Save and View
|
||||
</Button>
|
||||
</Stack>
|
||||
<InputGroup>
|
||||
{!id || admin ? (
|
||||
<SingleFieldNumericInput label={'Steam ID'} textField={'steamId'} field={game.steamId} bg={bg}
|
||||
handleInputChange={handleInputChange} required={game.scrape}/>
|
||||
) : ''}
|
||||
{!id && (
|
||||
<FormControl display="flex" ml={'3'} alignItems="center">
|
||||
<FormLabel htmlFor="scrape" mb="0">Steam Scrape</FormLabel>
|
||||
<Switch
|
||||
isChecked={game.scrape}
|
||||
onChange={() => {
|
||||
handleToggle('scrape')
|
||||
}}
|
||||
name="scrape"
|
||||
id="scrape"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
</InputGroup>
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
|
||||
<SingleFieldInput label={'Title'} textField={'title'} field={game.title} bg={bg}
|
||||
handleInputChange={handleInputChange} required={false}/>
|
||||
) : ''}
|
||||
<>
|
||||
<FormControl mb="3">
|
||||
<FormLabel>Series</FormLabel>
|
||||
<Box
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md">
|
||||
<CreatableSelect
|
||||
name="series"
|
||||
isClearable
|
||||
onChange={(values) => {
|
||||
if (values === null) values = {value: '', label: ''}
|
||||
if (values) {
|
||||
handleInputChange('series', values.value)
|
||||
}
|
||||
}}
|
||||
options={tags.series}
|
||||
value={{value: game.series, label: game.series}}
|
||||
/>
|
||||
</Box>
|
||||
</FormControl>
|
||||
</>
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
|
||||
<>
|
||||
<SingleFieldInput label={'Front Image'} textField={'frontImage'} field={game.frontImage}
|
||||
bg={bg}
|
||||
handleInputChange={handleInputChange} required={false}/>
|
||||
<FormControl mb="3">
|
||||
<FormLabel>Screenshots</FormLabel>
|
||||
<Box
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md">
|
||||
<CreatableSelect
|
||||
name="screenshots"
|
||||
isClearable
|
||||
isMulti
|
||||
onChange={(
|
||||
values: Options<{
|
||||
value: string
|
||||
label: string
|
||||
}>,
|
||||
) => {
|
||||
if (values) {
|
||||
const valueArray = values
|
||||
.reduce(
|
||||
(
|
||||
prev: {
|
||||
id: number
|
||||
full: string
|
||||
thumbnail: string
|
||||
}[],
|
||||
curr,
|
||||
index,
|
||||
) => {
|
||||
prev.push({
|
||||
id: index,
|
||||
full: curr.value,
|
||||
thumbnail: curr.value,
|
||||
})
|
||||
return prev
|
||||
},
|
||||
[],
|
||||
)
|
||||
.filter((x) => x.full !== 'Copy/Paste Image URL and Press Enter')
|
||||
handleInputChange('screenshots', valueArray)
|
||||
}
|
||||
}}
|
||||
placeholder="Copy/Paste Image URL and Press Enter"
|
||||
value={screenshots[0]?.value === '' ? [] : screenshots}
|
||||
/>
|
||||
</Box>
|
||||
</FormControl>
|
||||
</>
|
||||
) : ''}
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1) ?
|
||||
<CreatableSelectInput label={'Genres'} textField={'genre'}
|
||||
value={genres[0]?.value === '' ? [] : genres}
|
||||
bg={bg}
|
||||
handleInputChange={handleInputChange} options={tags.genres}/>
|
||||
: ''}
|
||||
<>
|
||||
<CreatableSelectInput label={'Store'} textField={'store'}
|
||||
value={store[0]?.value === '' ? [] : store}
|
||||
bg={bg}
|
||||
handleInputChange={handleAccessedBy} options={tags.store}/>
|
||||
<SelectInput label={'Play Status'} textField={'playStatus'}
|
||||
field={game.accessedBy[0].playStatus} bg={bg}
|
||||
handleAccessedBy={handleAccessedBy} options={playStatusValues}/>
|
||||
</>
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
|
||||
<Box mt="5" mb="5">
|
||||
<FormLabel>Operating Systems</FormLabel>
|
||||
<Box
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
p="2">
|
||||
<Stack spacing={[1, 5]} direction={['column', 'row']}>
|
||||
<CheckboxInput label={'Windows'} textField={'windows'} checked={game.os.windows}
|
||||
defaultIsChecked={true} handleOSChange={handleOSChange}/>
|
||||
<CheckboxInput label={'Mac OSX'} textField={'mac'} checked={game.os.mac}
|
||||
defaultIsChecked={false}
|
||||
handleOSChange={handleOSChange}/>
|
||||
<CheckboxInput label={'Linux'} textField={'linux'} checked={game.os.linux}
|
||||
defaultIsChecked={false}
|
||||
handleOSChange={handleOSChange}/>
|
||||
<CheckboxInput label={'Android'} textField={'android'} checked={game.os.android}
|
||||
defaultIsChecked={false}
|
||||
handleOSChange={handleOSChange}/>
|
||||
<CheckboxInput label={'iOS'} textField={'ios'} checked={game.os.ios}
|
||||
defaultIsChecked={false}
|
||||
handleOSChange={handleOSChange}/>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
) : ''}
|
||||
<>
|
||||
<SelectInput label={'Does it work with WINE?'} textField={'wine'} field={game.wine} bg={bg}
|
||||
handleAccessedBy={handleInputChange} options={wineIntelValues}/>
|
||||
<SelectInput label={'Does it work on an Intel Graphics Card?'} textField={'intel'}
|
||||
field={game.intel}
|
||||
bg={bg}
|
||||
handleAccessedBy={handleInputChange} options={wineIntelValues}/>
|
||||
</>
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
|
||||
<>
|
||||
<SelectInput label={'Does it support a controller?'} textField={'controller'}
|
||||
field={game.controller}
|
||||
bg={bg}
|
||||
handleAccessedBy={handleInputChange} options={controllerValues}/>
|
||||
<CreatableSelectInput label={'Developers'} textField={'developer'}
|
||||
value={developer[0]?.value === '' ? [] : developer} bg={bg}
|
||||
handleInputChange={handleInputChange} options={tags.developer}/>
|
||||
<CreatableSelectInput label={'Publishers'} textField={'publisher'}
|
||||
value={publisher[0]?.value === '' ? [] : publisher} bg={bg}
|
||||
handleInputChange={handleInputChange} options={tags.publisher}/>
|
||||
<SingleFieldInput label={'Release Date'} textField={'releaseDate'} field={game.releaseDate}
|
||||
bg={bg}
|
||||
handleInputChange={handleInputChange} required={game.scrape}/>
|
||||
<TextareaInput label={'Short Description'} textField={'shortDesc'} field={game.shortDesc}
|
||||
bg={bg}
|
||||
handleInputChange={handleInputChange} rows={3}/>
|
||||
<TextareaInput label={'Reviews'} textField={'reviews'} field={game.reviews} bg={bg}
|
||||
handleInputChange={handleInputChange} rows={5}/>
|
||||
<TextareaInput label={'Summary'} textField={'summary'} field={game.summary} bg={bg}
|
||||
handleInputChange={handleInputChange} rows={5}/>
|
||||
</>
|
||||
) : ''}
|
||||
<>
|
||||
<SelectInput label={'Do you own the Soundtrack?'} textField={'soundtrack'}
|
||||
field={game.accessedBy[0].soundtrack} bg={bg}
|
||||
handleAccessedBy={handleAccessedBy} options={yesNo}/>
|
||||
</>
|
||||
{(gameId === '' && !game.scrape) || (gameId.length >= 1 && admin) ? (
|
||||
<>
|
||||
<Text align="center" fontSize="xl" mt="10">System Requirements</Text>
|
||||
<Box>
|
||||
<Text fontSize="lg" align="center">Windows</Text>
|
||||
<SystemRequirementsTextareaInput label={'Windows Minimum'}
|
||||
textField={'systemRequirements.windows.minimum'}
|
||||
field={game.systemRequirements.windows.minimum} bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
<SystemRequirementsTextareaInput label={'Windows Recommended'}
|
||||
textField={'systemRequirements.windows.recommended'}
|
||||
field={game.systemRequirements.windows.recommended}
|
||||
bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
<Text fontSize="lg" align="center">Mac OSX</Text>
|
||||
<SystemRequirementsTextareaInput label={'Mac OSX Minimum'}
|
||||
textField={'systemRequirements.mac.minimum'}
|
||||
field={game.systemRequirements.mac.minimum} bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
<SystemRequirementsTextareaInput label={'Mac OSX Recommended'}
|
||||
textField={'systemRequirements.mac.recommended'}
|
||||
field={game.systemRequirements.mac.recommended} bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
<Text fontSize="lg" align="center">Linux</Text>
|
||||
<SystemRequirementsTextareaInput label={'Linux Minimum'}
|
||||
textField={'systemRequirements.linux.minimum'}
|
||||
field={game.systemRequirements.linux.minimum} bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
<SystemRequirementsTextareaInput label={'Linux Recommended'}
|
||||
textField={'systemRequirements.linux.recommended'}
|
||||
field={game.systemRequirements.linux.recommended}
|
||||
bg={bg}
|
||||
handleSystemRequirements={handleSystemRequirements}
|
||||
rows={3}/>
|
||||
</Box>
|
||||
</>
|
||||
) : ''}
|
||||
<Stack spacing={[1, 5]} direction={{base: 'column', sm: 'row'}} align={{base: 'start'}}>
|
||||
<Button
|
||||
isLoading={submitLoading}
|
||||
colorScheme="blue"
|
||||
type="submit"
|
||||
data-name={"save"}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={moveOn}
|
||||
colorScheme="green"
|
||||
type="submit"
|
||||
data-name={"save-and-view"}
|
||||
>
|
||||
Save and View
|
||||
</Button>
|
||||
{id && (
|
||||
<>
|
||||
<Spacer/>
|
||||
<Button
|
||||
colorScheme="red"
|
||||
onMouseDown={() => {
|
||||
openState.set(true)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GameForm
|
78
src/components/GamesDashboard.tsx
Normal file
78
src/components/GamesDashboard.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Container maxW="container.xl" mt="7">
|
||||
<Pagination />
|
||||
<ActiveFilters />
|
||||
<Divider />
|
||||
{!loaded ? <DashboardLoadingModal /> : games.count.totalGames === 0 ?
|
||||
<Center h={'80vh'}>
|
||||
<VStack spacing={10}>
|
||||
<Heading as={'h1'} size={'3xl'}>No Results Found...</Heading>
|
||||
<Divider />
|
||||
{games.searchParams !== '' ?
|
||||
<Box mt='10'>
|
||||
<ResetFilterButton text='Clear Search and Filters' size='lg' variant='outline' colorScheme='blue' />
|
||||
</Box>
|
||||
:
|
||||
<Box
|
||||
rounded={'md'}
|
||||
as={NavLink}
|
||||
border={'1px'}
|
||||
p={'3'}
|
||||
fontSize={'2xl'}
|
||||
to="/games/create">
|
||||
Create a Game
|
||||
</Box>
|
||||
}
|
||||
</VStack>
|
||||
</Center>
|
||||
:
|
||||
<SimpleGrid columns={{ base: 1, sm: 2, md: 3 }} spacing={10} style={{ marginTop: '3rem' }}>
|
||||
{games.data.map((game) => (
|
||||
<GameThumbnailContextMenu
|
||||
key={game._id}
|
||||
//@ts-ignore
|
||||
game={game} />
|
||||
))}
|
||||
</SimpleGrid>}
|
||||
<Divider my={'1rem'} />
|
||||
<Pagination />
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GameDashboard
|
252
src/components/Header.tsx
Normal file
252
src/components/Header.tsx
Normal file
@ -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 ? <SearchModal/> : ''}
|
||||
<Flex
|
||||
zIndex={10000000}
|
||||
bg={bgHeader}
|
||||
m="0"
|
||||
as="header"
|
||||
px="2"
|
||||
w="100%"
|
||||
h={16}
|
||||
top={0}
|
||||
alignItems={'center'}
|
||||
position={loadedPreferences.get().stickyNav ? "fixed" : "relative"}
|
||||
justifyContent={'space-between'}
|
||||
>
|
||||
<HStack spacing={8} alignItems={'center'}>
|
||||
<Box as={RouteLink} to="/">
|
||||
Game Database
|
||||
</Box>
|
||||
<HStack
|
||||
as={'nav'}
|
||||
spacing={4}
|
||||
display={{ base: 'none', md: 'flex' }}
|
||||
>
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: bgLink,
|
||||
}}
|
||||
as={RouteLink}
|
||||
to="/games">
|
||||
Games
|
||||
</Link>
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: bgLink,
|
||||
}}
|
||||
as={RouteLink}
|
||||
to="/games/create">
|
||||
Create Game
|
||||
</Link>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Spacer/>
|
||||
{isSmallerThan768 ?
|
||||
<>
|
||||
<Button ref={btnRef} onMouseDown={mobileIsOpen ? mobileOnClose : mobileOnOpen}>
|
||||
<HamburgerIcon/>
|
||||
</Button>
|
||||
<MobileBar mobileIsOpen={mobileIsOpen} mobileOnClose={mobileOnClose} btnRef={btnRef} bgLink={bgLink}
|
||||
user={user} colorMode={colorMode} toggleColorMode={toggleColorMode}/>
|
||||
</>
|
||||
:
|
||||
<Stack direction={'row'}>
|
||||
|
||||
{ user.role === 'admin' && <AdminMode/>}
|
||||
<Tooltip hasArrow mr={'2'} label={'Filters'} aria-label={'Filters'}>
|
||||
<Button
|
||||
rounded={'full'}
|
||||
bg={'transparent'}
|
||||
ref={btnRef}
|
||||
minW={0}
|
||||
onMouseDown={async () => {
|
||||
if (filterIsOpen) {
|
||||
filterLoadingState.set(false)
|
||||
filterOnClose()
|
||||
} else {
|
||||
filterLoadingState.set(true)
|
||||
filterOnOpen()
|
||||
}
|
||||
}}>
|
||||
<Icon as={BiFilterAlt}/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<FilterBar filterIsOpen={filterIsOpen} filterOnClose={filterOnClose} btnRef={btnRef} filterLoadingState={filterLoadingState} />
|
||||
|
||||
<Tooltip hasArrow mr={'2'} label={'Search: ctrl+s'} aria-label={'Search'}>
|
||||
<Button
|
||||
rounded={'full'}
|
||||
onMouseDown={() => {
|
||||
searchModalOpen ? setSearchModalOpen.set(false) : setSearchModalOpen.set(true)
|
||||
}}
|
||||
bg={'transparent'}
|
||||
cursor={'pointer'}
|
||||
minW={0}>
|
||||
<Search2Icon/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Menu>
|
||||
<Tooltip hasArrow mr={'2'} label={'Settings'} aria-label={'Settings'}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded={'full'}
|
||||
variant={'transparent'}
|
||||
cursor={'pointer'}
|
||||
minW={0}>
|
||||
<SettingsIcon/>
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList alignItems={'center'}>
|
||||
<VStack>
|
||||
{/*<Button onMouseDown={toggleColorMode} w={'70%'}>*/}
|
||||
{/* {colorMode === 'light' ? <MoonIcon/> : <SunIcon/>}*/}
|
||||
{/* {colorMode === 'light' ?*/}
|
||||
{/* <Text mt={'1'} ml={'2'}>Dark</Text> :*/}
|
||||
{/* <Text mt={'1'} ml={'2'}>Light</Text>}*/}
|
||||
{/*</Button>*/}
|
||||
<Button
|
||||
variant={'solid'}
|
||||
colorScheme={'blue'}
|
||||
size={'md'}
|
||||
leftIcon={<AddIcon/>}
|
||||
as={RouteLink}
|
||||
w={'70%'}
|
||||
to="/games/create">
|
||||
<Text mt="1">Create Game</Text>
|
||||
</Button>
|
||||
<Divider/>
|
||||
<Heading as={'h3'} size={'md'}>Keyboard Shortcuts</Heading>
|
||||
<HStack><Text fontSize={'sm'}>Search:</Text><span><Kbd>ctrl</Kbd> + <Kbd>,</Kbd></span></HStack>
|
||||
<HStack><Text fontSize={'sm'}>Games Grid:</Text><span><Kbd>ctrl</Kbd> + <Kbd>.</Kbd></span></HStack>
|
||||
<HStack><Text fontSize={'sm'}>Create
|
||||
Game:</Text><span><Kbd>ctrl</Kbd> + <Kbd>/</Kbd></span></HStack>
|
||||
</VStack>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Menu>
|
||||
<Tooltip hasArrow mr={'2'} label={'User Info and Logout'} aria-label={'User Info'}>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rounded={'full'}
|
||||
variant={'transparent'}
|
||||
cursor={'pointer'}
|
||||
minW={0}>
|
||||
<Avatar size={'sm'} src={user.avatar}/>
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<MenuList alignItems={'center'}>
|
||||
<br/>
|
||||
<Center>
|
||||
<Avatar
|
||||
size={'2xl'}
|
||||
src={user.avatar}
|
||||
/>
|
||||
</Center>
|
||||
<br/>
|
||||
<Center>
|
||||
<p>{user.displayName}</p>
|
||||
</Center>
|
||||
<br/>
|
||||
<MenuDivider/>
|
||||
<MenuItem onMouseDown={() => history.push('/profile')}>Preferences</MenuItem>
|
||||
<MenuItem onMouseDown={() => logout()}><Icon as={MdOutlinePowerSettingsNew} mr={'2'}/> <Text
|
||||
mt={'1'}>Logout</Text></MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Stack>
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
21
src/components/Home.tsx
Normal file
21
src/components/Home.tsx
Normal file
@ -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 (
|
||||
<Flex align={'center'} bgGradient={'linear(to-br, #314755, #26a0da)'} height={'100vh'}>
|
||||
<Center w={'100%'}>
|
||||
<VStack spacing={4} align={'center'} margin={'auto'}>
|
||||
<Image src="/assets/Logo.svg" alt="Games Database Logo"/>
|
||||
<Heading as="h2" fontSize={'1.7em'} fontWeight={'normal'}>Welcome to your Games Database</Heading>
|
||||
<Button as={Link} to="/login" size='lg' variant='outline' fontSize='1.25rem'>
|
||||
Login
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
21
src/components/LoadingModal.tsx
Normal file
21
src/components/LoadingModal.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import { Center, CircularProgress } from '@chakra-ui/react'
|
||||
|
||||
export default function LoadingModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Center
|
||||
pos="absolute"
|
||||
w="100%"
|
||||
h="100vh"
|
||||
top={'0'}
|
||||
right={'0'}
|
||||
bottom={'0'}
|
||||
left={'0'}
|
||||
>
|
||||
<CircularProgress isIndeterminate color="blue.500"/>
|
||||
</Center>
|
||||
</>
|
||||
)
|
||||
}
|
134
src/components/MobileBar.tsx
Normal file
134
src/components/MobileBar.tsx
Normal file
@ -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<null>
|
||||
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 (
|
||||
<Drawer
|
||||
isOpen={mobileIsOpen}
|
||||
placement="right"
|
||||
onClose={mobileOnClose}
|
||||
finalFocusRef={btnRef}
|
||||
>
|
||||
<DrawerOverlay/>
|
||||
<DrawerContent>
|
||||
<DrawerCloseButton/>
|
||||
<DrawerHeader>
|
||||
<Image maxW="15rem" src="/assets/Logo.svg" alt="Games Database Logo"/>
|
||||
</DrawerHeader>
|
||||
|
||||
<DrawerBody>
|
||||
<AdminMode />
|
||||
<Stack
|
||||
as={'nav'}
|
||||
spacing={4}
|
||||
>
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: bgLink,
|
||||
}}
|
||||
as={NavLink}
|
||||
to="/games">
|
||||
<Text align={'right'}>Games</Text>
|
||||
</Link>
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: bgLink,
|
||||
}}
|
||||
as={NavLink}
|
||||
to="/games/create">
|
||||
<Text align={'right'}>Create Game</Text>
|
||||
</Link>
|
||||
<Divider/>
|
||||
<Search/>
|
||||
{searchCompleted.get() && <Text>Games Found: {games.count.totalGames}</Text>}
|
||||
<Divider/>
|
||||
<Center><Avatar mr={'2'} size={'sm'} src={user.avatar}/><Text mt={1}>{user.displayName}</Text></Center>
|
||||
<Link
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
bg: bgLink,
|
||||
}}
|
||||
as={NavLink}
|
||||
to="/profile">
|
||||
<Text align={'right'}>Preferences</Text>
|
||||
</Link>
|
||||
<Divider/>
|
||||
{/*<Button onMouseDown={toggleColorMode} ml={'auto'}>*/}
|
||||
{/* {colorMode === 'light' ? <MoonIcon/> : <SunIcon/>}*/}
|
||||
{/* {colorMode === 'light' ?*/}
|
||||
{/* <Text mt={'1'} ml={'2'}>Dark</Text> :*/}
|
||||
{/* <Text mt={'1'} ml={'2'}>Light</Text>}*/}
|
||||
{/*</Button>*/}
|
||||
{/*<Divider/>*/}
|
||||
<ResetFilterButton />
|
||||
</Stack>
|
||||
|
||||
|
||||
</DrawerBody>
|
||||
|
||||
<DrawerFooter>
|
||||
<Button
|
||||
variant={'solid'}
|
||||
colorScheme={'blue'}
|
||||
size={'md'}
|
||||
leftIcon={<AddIcon/>}
|
||||
as={NavLink}
|
||||
mr={3}
|
||||
to="/games/create">
|
||||
<Text mt="1">Create Game</Text>
|
||||
</Button>
|
||||
<Button onMouseDown={() => logout()}><Icon as={AiOutlineLogout} mr={'2'}/> <Text
|
||||
mt={'1'}>Logout</Text></Button>
|
||||
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default MobileBar
|
18
src/components/NoGames.tsx
Normal file
18
src/components/NoGames.tsx
Normal file
@ -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 (
|
||||
<Flex align={'center'} bgGradient={'linear(to-br, #314755, #26a0da)'} height={'100vh'}>
|
||||
<Center w={'100%'}>
|
||||
<VStack spacing={4} align={'center'} margin={'auto'}>
|
||||
{<Heading as="h2">You have not added any games yet.</Heading>}
|
||||
<Button as={Link} to='/games/create' size='lg' variant='outline' fontSize='1.25rem'>
|
||||
Click here to add some!
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
</Flex>
|
||||
)
|
||||
}
|
205
src/components/Pagination.tsx
Normal file
205
src/components/Pagination.tsx
Normal file
@ -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 (
|
||||
<Grid mb={'6'} justifyItems={{
|
||||
base: 'center'
|
||||
}} alignItems={{
|
||||
base: 'center'
|
||||
}} templateAreas={{
|
||||
base: `
|
||||
"backArrows pageNumber forwardArrows"
|
||||
"backArrows countPerPage forwardArrows"
|
||||
"backArrows gamesFound forwardArrows"
|
||||
`,
|
||||
md: `"backArrows pageNumber goToPage countPerPage gamesFound forwardArrows"`
|
||||
}}>
|
||||
<GridItem area={"backArrows"}>
|
||||
<Flex>
|
||||
<Tooltip label="First Page">
|
||||
<IconButton
|
||||
aria-label={'First Page'}
|
||||
onMouseDown={() => gotoPage(0)}
|
||||
isDisabled={pageIndex <= 1}
|
||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||
mr={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Previous Page">
|
||||
<IconButton
|
||||
aria-label={'Previous Page'}
|
||||
onMouseDown={previousPage}
|
||||
isDisabled={pageIndex <= 1}
|
||||
icon={<ChevronLeftIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
<GridItem area={"pageNumber"}>
|
||||
<Text mr={{ md: 8 }}>
|
||||
Page{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{pageIndex}
|
||||
</Text>{' '}
|
||||
of{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{pageCount}
|
||||
</Text>
|
||||
</Text>
|
||||
</GridItem>
|
||||
{!isSmallerThan768 &&
|
||||
<GridItem area={"goToPage"}>
|
||||
<Text mb={'1'}>Go to Page:</Text>
|
||||
<NumberInput
|
||||
bg={bg}
|
||||
rounded={'md'}
|
||||
w={{ base: 20, md: 28 }}
|
||||
min={1}
|
||||
max={pageCount}
|
||||
value={pageIndex}
|
||||
onChange={(value) => {
|
||||
const page = value ? Number(value) : 1
|
||||
gotoPage(page)
|
||||
}}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
|
||||
</GridItem>
|
||||
}
|
||||
<GridItem area={"countPerPage"}>
|
||||
<Text mb={'1'}>Count Per Page:</Text>
|
||||
<Select
|
||||
ml={{
|
||||
base: '4',
|
||||
md: '0'
|
||||
}}
|
||||
bg={bg}
|
||||
rounded={'md'}
|
||||
w={{ base: 20, md: 32 }}
|
||||
value={pageSize}
|
||||
onChange={(e) => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
{pageSize}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</GridItem>
|
||||
<GridItem area={'gamesFound'}>
|
||||
<Text>Games Found: {games.count.totalGames}</Text>
|
||||
</GridItem>
|
||||
<GridItem area={"forwardArrows"}>
|
||||
<Flex justifySelf={'end'}>
|
||||
<Tooltip label="Next Page">
|
||||
<IconButton
|
||||
aria-label={'Next Page'}
|
||||
onMouseDown={nextPage}
|
||||
isDisabled={pageIndex >= pageCount}
|
||||
icon={<ChevronRightIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Last Page">
|
||||
<IconButton
|
||||
aria-label={'Last Page'}
|
||||
onMouseDown={() => gotoPage(pageCount)}
|
||||
isDisabled={pageIndex >= pageCount}
|
||||
icon={<ArrowRightIcon h={3} w={3} />}
|
||||
ml={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
)
|
||||
}
|
88
src/components/Search.tsx
Normal file
88
src/components/Search.tsx
Normal file
@ -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<HTMLFormElement>) => {
|
||||
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 = <G extends keyof GameList>(name: G, value: string) => {
|
||||
gamesState.merge(() => ({ [name]: value }))
|
||||
ChangeActiveFilterHeader(games.filters, games.searchParams, setShowFiltersModuleState)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormControl mb="3">
|
||||
<InputGroup>
|
||||
<Input
|
||||
autoFocus
|
||||
id="searchParams"
|
||||
name="searchParams"
|
||||
bg={bg}
|
||||
type="text"
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Search..."
|
||||
value={games.searchParams}
|
||||
onChange={(e) => {
|
||||
handleInputChange('searchParams', e.target.value)
|
||||
}}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<Button
|
||||
isLoading={submitLoading}
|
||||
type="submit"
|
||||
colorScheme="blue"
|
||||
>
|
||||
<SearchIcon/>
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
29
src/components/SearchModal.tsx
Normal file
29
src/components/SearchModal.tsx
Normal file
@ -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 (
|
||||
<Modal isOpen={isOpen} onClose={handleClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader>Search</ModalHeader>
|
||||
<ModalCloseButton onMouseDown={() => handleClose()}/>
|
||||
<ModalBody>
|
||||
<Search />
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
15
src/components/authComponents/AdminMode.tsx
Normal file
15
src/components/authComponents/AdminMode.tsx
Normal file
@ -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 (
|
||||
<Checkbox size={'lg'} onChange={() => adminModeToggle()} isChecked={admin} isDisabled={!(user.role === 'admin')}>
|
||||
<Text mt={'1'}>Administrator Mode</Text>
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
74
src/components/authComponents/ForgotPassword.tsx
Normal file
74
src/components/authComponents/ForgotPassword.tsx
Normal file
@ -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 (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}>
|
||||
<FormLabel htmlFor="email">Email</FormLabel>
|
||||
<Input
|
||||
id="email"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Email"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.email?.type === 'required' && errors.email.message?.toString()}
|
||||
{errors.email?.type === 'pattern' && 'Invalid Email Address'}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<br/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
94
src/components/authComponents/LoginForm.tsx
Normal file
94
src/components/authComponents/LoginForm.tsx
Normal file
@ -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 (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}>
|
||||
<FormLabel htmlFor="email">Email</FormLabel>
|
||||
<Input
|
||||
id="email"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Email"
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
pattern: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.email?.type === 'required' && errors.email.message?.toString()}
|
||||
{errors.email?.type === 'pattern' && 'Invalid Email Address'}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}>
|
||||
<FormLabel htmlFor="password">Password</FormLabel>
|
||||
<Input
|
||||
id="password"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...register('password', { required: 'You must specify a password' })}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.password && errors.password.message?.toString()}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<br/>
|
||||
<HStack>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</HStack>
|
||||
<Center mt={'5rem'}>
|
||||
<Button as={Link} to={'/forgotpassword'}>Forgot Password</Button>
|
||||
</Center>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
120
src/components/authComponents/ResetPassword.tsx
Normal file
120
src/components/authComponents/ResetPassword.tsx
Normal file
@ -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<URL>()
|
||||
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 (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}>
|
||||
<FormLabel htmlFor="password">Password</FormLabel>
|
||||
<Input
|
||||
id="password"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
{...register('password', {
|
||||
required: 'You must specify a password',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: 'Password must have at least 8 characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.password && errors.password.message?.toString()}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isInvalid={Boolean(errors.repeat_password)} mb={'3'}>
|
||||
<FormLabel htmlFor="repeat_password">Repeat Password</FormLabel>
|
||||
<Input
|
||||
id="repeat_password"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Repeat Password"
|
||||
type="password"
|
||||
{...register('repeat_password', {
|
||||
validate: (value) =>
|
||||
value === password.current || 'The passwords do not match',
|
||||
})}
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{errors.repeat_password && errors.repeat_password.message?.toString()}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<br/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
16
src/components/dashboardComponents/DashboardLoadingModal.tsx
Normal file
16
src/components/dashboardComponents/DashboardLoadingModal.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Center, CircularProgress } from '@chakra-ui/react'
|
||||
|
||||
export default function DashboardLoadingModal() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Center
|
||||
w="100%"
|
||||
mt={'1rem'}
|
||||
>
|
||||
<CircularProgress isIndeterminate color="blue.500"/>
|
||||
</Center>
|
||||
</>
|
||||
)
|
||||
}
|
44
src/components/dashboardComponents/GameThumbnail.tsx
Normal file
44
src/components/dashboardComponents/GameThumbnail.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<LinkBox
|
||||
as={Link}
|
||||
to={`/games/${gameLink}`}
|
||||
onMouseOver={onToggle}
|
||||
onMouseOut={onToggle}
|
||||
>
|
||||
<LinkOverlay>
|
||||
<Image borderTopRadius="md" src={game.frontImage} alt={game.title} />
|
||||
</LinkOverlay>
|
||||
<Heading m={2} size="md" my="2">
|
||||
{game.steamId !== '' ? `${game.steamId}: ${game.title}` : `${game.title}`}
|
||||
</Heading>
|
||||
<Text ml={2}>Play Status: {playState}</Text>
|
||||
</LinkBox>
|
||||
<Flex m={2}>
|
||||
<GameRating game={game} size={'1em'} />
|
||||
<Spacer />
|
||||
{game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 && (
|
||||
<ChakraLink href={`steam://run/${game.steamId}`}>
|
||||
<SteamAndDeckLogo size={'2em'} />
|
||||
</ChakraLink>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<ContextMenu<HTMLDivElement> key={game._id} renderMenu={() => (
|
||||
<MenuList>
|
||||
<MenuItem><Link to={`/games/${gameLink}`} target="_blank" rel="noopener noreferrer">Open in New Tab</Link></MenuItem>
|
||||
<MenuGroup title={'Change PlayStatus'}>
|
||||
{playStatusValues.map((value, index) => <MenuItem
|
||||
key={index}
|
||||
onClick={() => rightClick(value.label, value.value)}
|
||||
>{value.label}</MenuItem>)}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
)}>
|
||||
{ref => <>
|
||||
<Box
|
||||
ref={ref}
|
||||
border={'1px'}
|
||||
borderColor={'gray.500'}
|
||||
maxW={'sm'}
|
||||
rounded={'md'}
|
||||
transitionProperty={'box-shadow transform'}
|
||||
transitionDuration={'1'}
|
||||
transitionTimingFunction={'ease-in-out'}
|
||||
_hover={{ boxShadow: '2xl', transform: 'translateY(-3px)' }}
|
||||
>
|
||||
<GameThumbnail onToggle={onToggle} game={game} playState={playState} />
|
||||
</Box>
|
||||
{(!mql.matches) && <GameThumbnailSlide isOpen={isOpen} game={game} />}
|
||||
</>
|
||||
}
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameThumbnailContextMenu;
|
36
src/components/dashboardComponents/GameThumbnailSlide.tsx
Normal file
36
src/components/dashboardComponents/GameThumbnailSlide.tsx
Normal file
@ -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 (
|
||||
<Slide direction="bottom" in={isOpen} unmountOnExit={true} style={{zIndex: 10}}>
|
||||
<Box
|
||||
p="40px"
|
||||
color="white"
|
||||
mt="4"
|
||||
bgGradient={'linear(to-br, #314755, #26a0da)'}
|
||||
borderTopRadius="md"
|
||||
shadow="md"
|
||||
>
|
||||
<Container maxW="container.xl">
|
||||
<HStack spacing='2rem'>
|
||||
<ImageSlider game={game} width={'250px'}/>
|
||||
<Box>
|
||||
<Heading>{game.title}</Heading>
|
||||
{game.shortDesc}
|
||||
</Box>
|
||||
</HStack>
|
||||
</Container>
|
||||
</Box>
|
||||
</Slide>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameThumbnailSlide;
|
321
src/components/detailsComponents/Features.tsx
Normal file
321
src/components/detailsComponents/Features.tsx
Normal file
@ -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<Data>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box mb="3">
|
||||
<Heading as="h3" size="lg">Features and Information</Heading>
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Rating:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<GameRating game={game} size={'1.5em'} />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
{game.accessedBy[0].store.includes('Steam') && game.steamId.length > 0 &&
|
||||
(<>
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Steam ID:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link pt="1" href={`steam://run/${game.steamId}`}>{game.steamId}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
</>)}
|
||||
{game.series.length > 1 &&
|
||||
(<>
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Series:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link onMouseDown={() => {
|
||||
gamesState.filters.series.set(game.series)
|
||||
handleOnClick()
|
||||
}} pt="1">{game.series}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
</>)}
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Play Status:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Box display={'flex'}>
|
||||
{editMode ?
|
||||
<Box display={'flex'}>
|
||||
<GamePlayStatus game={game} playState={playState} setPlayState={setPlayState} />
|
||||
<Link ml={'1'} mt={'1'} onMouseDown={() => setEditMode(!editMode)}>
|
||||
<BiSave size={'1.5rem'} />
|
||||
</Link>
|
||||
</Box>
|
||||
:
|
||||
<Box display={'flex'}>
|
||||
<Link mr={'5px'} onMouseDown={() => {
|
||||
gamesState.filters.playStatus.set(game.accessedBy[0].playStatus)
|
||||
handleOnClick()
|
||||
}} pt="1">
|
||||
{playState}
|
||||
</Link>
|
||||
<Link mt={'1.5'} onMouseDown={() => setEditMode(!editMode)}>
|
||||
<BsPencil />
|
||||
</Link>
|
||||
</Box>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Genre:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt={'2'} ml={'4'}>
|
||||
{game.genre.map((genre, index) => [
|
||||
index > 0 && ", ",
|
||||
<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.genre.set(genre)
|
||||
handleOnClick()
|
||||
}}>
|
||||
{genre}
|
||||
</Link>
|
||||
])}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Operating Systems:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Text pt="1">
|
||||
{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(<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.os.set(os)
|
||||
handleOnClick()
|
||||
}}>Mac OSX</Link>)
|
||||
else if (os === 'ios') osStr.push(<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.os.set(os)
|
||||
handleOnClick()
|
||||
}}>iOS</Link>)
|
||||
else osStr.push(<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.os.set(os)
|
||||
handleOnClick()
|
||||
}}>{os.charAt(0).toUpperCase() + os.slice(1)}</Link>)
|
||||
return osStr
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Store:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
{game.accessedBy[0].store.map((store, index) => [
|
||||
index > 0 && ", ",
|
||||
<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.store.set(store)
|
||||
handleOnClick()
|
||||
}}>
|
||||
{store}
|
||||
</Link>
|
||||
])}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Developer:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
{game.developer.map((developer, index) => [
|
||||
index > 0 && ", ",
|
||||
<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.developer.set(developer)
|
||||
handleOnClick()
|
||||
}}>
|
||||
{developer}
|
||||
</Link>
|
||||
])}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Publisher:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
{game.publisher.map((publisher, index) => [
|
||||
index > 0 && ", ",
|
||||
<Link key={index} onMouseDown={() => {
|
||||
gamesState.filters.publisher.set(publisher)
|
||||
handleOnClick()
|
||||
}}>
|
||||
{publisher}
|
||||
</Link>
|
||||
])}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Controller Support:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link onMouseDown={() => {
|
||||
gamesState.filters.controller.set(game.controller)
|
||||
handleOnClick()
|
||||
}} pt="1">{game.controller}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Release Date:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Text pt="1">{game.releaseDate}</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Soundtrack:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link onMouseDown={() => {
|
||||
gamesState.filters.soundtrack.set(game.accessedBy[0].soundtrack)
|
||||
handleOnClick()
|
||||
}} pt="1">{game.accessedBy[0].soundtrack}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Does it work with Intel?:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link onMouseDown={() => {
|
||||
gamesState.filters.intel.set(game.intel)
|
||||
handleOnClick()
|
||||
}} pt="1">{game.intel}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
<Flex direction="column" m="2">
|
||||
<Box>
|
||||
<Badge colorScheme="blue" variant="outline" rounded="md" pl="2" pr="2">
|
||||
<Text pt="1" fontSize="sm">
|
||||
Does it work with WINE?:
|
||||
</Text>
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box mt="1.5" ml="5" mr="2">
|
||||
<Link onMouseDown={() => {
|
||||
gamesState.filters.wine.set(game.wine)
|
||||
handleOnClick()
|
||||
}} pt="1">{game.wine}</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Divider orientation="horizontal" />
|
||||
</Box>
|
||||
)
|
||||
}
|
51
src/components/detailsComponents/GamePlayStatus.tsx
Normal file
51
src/components/detailsComponents/GamePlayStatus.tsx
Normal file
@ -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 <Select
|
||||
id={'playStatus'}
|
||||
name={'playStatus'}
|
||||
value={{
|
||||
value: playState,
|
||||
label: playState,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if(e === null) return
|
||||
handleClick(e.value, e.label)
|
||||
}}
|
||||
size={'sm'}
|
||||
options={playStatusValues}
|
||||
chakraStyles={{
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: "150px"
|
||||
})
|
||||
}}
|
||||
/>
|
||||
};
|
||||
|
||||
export default GamePlayStatus;
|
52
src/components/detailsComponents/GameRating.tsx
Normal file
52
src/components/detailsComponents/GameRating.tsx
Normal file
@ -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 (
|
||||
<Flex>
|
||||
{/*@ts-ignore*/}
|
||||
<Rating
|
||||
start={0}
|
||||
stop={10}
|
||||
step={2}
|
||||
fractions={2}
|
||||
onMouseDown={handleClick}
|
||||
initialRating={finalRating}
|
||||
emptySymbol={<IoGameControllerOutline size={size}/>}
|
||||
fullSymbol={<IoGameController size={size} color={rating === 0 ? '#2761A7' : '#64ABDE'}/>}
|
||||
/>
|
||||
<Link mt={'1'} ml={'1.5'} onMouseDown={() => handleClick(0)}><BiReset /></Link>
|
||||
</Flex>
|
||||
)
|
||||
};
|
||||
|
||||
export default GameRating;
|
25
src/components/detailsComponents/ImageSlider.tsx
Normal file
25
src/components/detailsComponents/ImageSlider.tsx
Normal file
@ -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<Data>
|
||||
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 (
|
||||
<Carousel useKeyboardArrows width={width} swipeable emulateTouch autoPlay dynamicHeight={false} showThumbs={false} autoFocus infiniteLoop interval={2000}>
|
||||
{combinedImages.map(({ id, full }) => (
|
||||
<Image key={id} borderTopRadius="md" src={full} alt="Game Image" />
|
||||
),
|
||||
)}
|
||||
</Carousel>
|
||||
)
|
||||
}
|
35
src/components/detailsComponents/Summary.tsx
Normal file
35
src/components/detailsComponents/Summary.tsx
Normal file
@ -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<Data>
|
||||
}
|
||||
|
||||
export default function Summary({ game }: Props) {
|
||||
const { summary, reviews } = game
|
||||
return (
|
||||
<Box>
|
||||
{reviews.length >= 1 && (
|
||||
<>
|
||||
<Heading as="h1">Reviews</Heading>
|
||||
<Divider m="3" />
|
||||
<Interweave
|
||||
content={reviews}
|
||||
allowAttributes
|
||||
allowElements
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Heading mt={reviews.length >= 1 ? '10' : ''} as="h1">About This Game</Heading>
|
||||
<Divider m="3" />
|
||||
<Interweave
|
||||
content={summary}
|
||||
allowAttributes
|
||||
allowElements
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
34
src/components/detailsComponents/SummaryModal.tsx
Normal file
34
src/components/detailsComponents/SummaryModal.tsx
Normal file
@ -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<Data>
|
||||
}
|
||||
|
||||
const SummaryModal = ({ isOpen, onClose, game }: Props) => {
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'90%'}>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Summary game={game} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme='blue' onMouseDown={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummaryModal
|
70
src/components/detailsComponents/SystemRequirementsMenu.tsx
Normal file
70
src/components/detailsComponents/SystemRequirementsMenu.tsx
Normal file
@ -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<Data>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box>
|
||||
<Heading as='h3' size='lg'>System Requirements</Heading>
|
||||
<Tabs isLazy mt='3'>
|
||||
<TabList>
|
||||
<Tab>Windows</Tab>
|
||||
<Tab isDisabled={macMin}>Mac</Tab>
|
||||
<Tab isDisabled={linMin}>Linux</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Tabs isLazy>
|
||||
<TabList>
|
||||
<Tab>Minimum</Tab>
|
||||
<Tab isDisabled={winRec}>Recommended</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>{<Interweave content={windows.minimum} />}</TabPanel>
|
||||
<TabPanel>{<Interweave content={windows.recommended} />}</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Tabs isLazy>
|
||||
<TabList>
|
||||
<Tab isDisabled={macMin}>Minimum</Tab>
|
||||
<Tab isDisabled={macRec}>Recommended</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>{<Interweave content={mac.minimum} />}</TabPanel>
|
||||
<TabPanel>{<Interweave content={mac.recommended} />}</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Tabs isLazy>
|
||||
<TabList>
|
||||
<Tab isDisabled={linMin}>Minimum</Tab>
|
||||
<Tab isDisabled={linRec}>Recommended</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>{<Interweave content={linux.minimum} />}</TabPanel>
|
||||
<TabPanel>{<Interweave content={linux.recommended} />}</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
)
|
||||
}
|
34
src/components/detailsComponents/SystemRequirementsModal.tsx
Normal file
34
src/components/detailsComponents/SystemRequirementsModal.tsx
Normal file
@ -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<Data>
|
||||
}
|
||||
|
||||
const SummaryModal = ({ isOpen, onClose, game }: Props) => {
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxW={'90%'}>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<SystemRequirements game={game} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button colorScheme='blue' onMouseDown={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SummaryModal
|
26
src/components/formFields/CheckboxInput.tsx
Normal file
26
src/components/formFields/CheckboxInput.tsx
Normal file
@ -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 (
|
||||
<Checkbox
|
||||
defaultChecked={defaultIsChecked}
|
||||
isChecked={checked}
|
||||
onChange={() => {
|
||||
handleOSChange(textField)
|
||||
}}
|
||||
name={textField}
|
||||
spacing="1rem"
|
||||
><Text mt="1">{label}</Text></Checkbox>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckboxInput
|
46
src/components/formFields/CreatableSelectInput.tsx
Normal file
46
src/components/formFields/CreatableSelectInput.tsx
Normal file
@ -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 (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Box
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md">
|
||||
<CreatableSelect
|
||||
name={textField}
|
||||
isClearable
|
||||
isMulti
|
||||
onChange={(
|
||||
values: any,
|
||||
) => {
|
||||
if (values) {
|
||||
const valueArray = values.reduce((prev: string[], curr: any) => {
|
||||
prev.push(curr.value)
|
||||
return prev
|
||||
}, [])
|
||||
handleInputChange(textField, valueArray)
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
options={options}
|
||||
/>
|
||||
</Box>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatableSelectInput
|
42
src/components/formFields/SelectInput.tsx
Normal file
42
src/components/formFields/SelectInput.tsx
Normal file
@ -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 (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Box
|
||||
bg={bg}
|
||||
w={width}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md">
|
||||
<Select
|
||||
id={textField}
|
||||
name={textField}
|
||||
onChange={(values) => {
|
||||
if (values) handleAccessedBy(textField, values.value)
|
||||
}}
|
||||
options={options}
|
||||
value={{
|
||||
value: field,
|
||||
label: field,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectInput
|
37
src/components/formFields/SingleFieldInput.tsx
Normal file
37
src/components/formFields/SingleFieldInput.tsx
Normal file
@ -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: <G extends keyof Data>(name: string, value: Data[G]) => void
|
||||
required: boolean
|
||||
}
|
||||
|
||||
const SingleFieldInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => {
|
||||
|
||||
return (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Input
|
||||
id={textField}
|
||||
name={textField}
|
||||
bg={bg}
|
||||
type="text"
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
value={field}
|
||||
required={required}
|
||||
onChange={(e) => {
|
||||
handleInputChange(textField, e.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleFieldInput
|
39
src/components/formFields/SingleFieldNumericInput.tsx
Normal file
39
src/components/formFields/SingleFieldNumericInput.tsx
Normal file
@ -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: <G extends keyof Data>(name: string, value: Data[G]) => void
|
||||
required: boolean
|
||||
}
|
||||
|
||||
const SingleFieldNumericInput = ({ label, textField, field, bg, handleInputChange, required }: Props) => {
|
||||
|
||||
return (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Input
|
||||
id={textField}
|
||||
name={textField}
|
||||
bg={bg}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
value={field}
|
||||
required={required}
|
||||
onChange={(e) => {
|
||||
handleInputChange(textField, e.target.value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default SingleFieldNumericInput
|
@ -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 (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<JoditEditor
|
||||
ref={editor}
|
||||
value={field}
|
||||
config={config}
|
||||
onChange={(value) => {
|
||||
handleSystemRequirements(arrText[1], arrText[2], value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemRequirementsTextareaInput
|
42
src/components/formFields/TextareaInput.tsx
Normal file
42
src/components/formFields/TextareaInput.tsx
Normal file
@ -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: <G extends keyof Data>(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 (
|
||||
<FormControl mb="3">
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<JoditEditor
|
||||
ref={editor}
|
||||
value={field}
|
||||
config={config}
|
||||
onChange={(value) => {
|
||||
handleInputChange(textField, value)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextareaInput
|
67
src/components/helperComponents/ResetFilterButton.tsx
Normal file
67
src/components/helperComponents/ResetFilterButton.tsx
Normal file
@ -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 (
|
||||
<Button onMouseDown={resetFilters} size={size} variant={variant} colorScheme={colorScheme}>
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetFilterButton;
|
105
src/components/helperComponents/SteamAndDeckLogo.tsx
Normal file
105
src/components/helperComponents/SteamAndDeckLogo.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import {Box} from "@chakra-ui/react";
|
||||
|
||||
interface ISteamAndDeckLogo {
|
||||
size: string
|
||||
}
|
||||
|
||||
const SteamAndDeckLogo = (props: ISteamAndDeckLogo) => {
|
||||
return (
|
||||
<div>
|
||||
<Box
|
||||
as="svg"
|
||||
role="img"
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1080 1080"
|
||||
fill="#1f2127"
|
||||
_hover={{
|
||||
fill: "brand.accent",
|
||||
}}
|
||||
>
|
||||
<title>SteamAndDeckLogo</title>
|
||||
<path
|
||||
fill="url(#_Radial1)"
|
||||
d="M1080 216C1080 96.786 983.214 0 864 0H216C96.786 0 0 96.786 0 216v648c0 119.214 96.786 216 216 216h648c119.214 0 216-96.786 216-216V216z"
|
||||
></path>
|
||||
<clipPath id="_clip2">
|
||||
<path
|
||||
d="M1080 216C1080 96.786 983.214 0 864 0H216C96.786 0 0 96.786 0 216v648c0 119.214 96.786 216 216 216h648c119.214 0 216-96.786 216-216V216z"></path>
|
||||
</clipPath>
|
||||
<g clipPath="url(#_clip2)">
|
||||
<path
|
||||
fill="url(#_Linear3)"
|
||||
d="M326.9 728.816C280.975 678.813 253 612.591 253 540c0-155.64 128.6-282 287-282s287 126.36 287 282-128.6 282-287 282c-50.843 0-98.616-13.018-140.051-35.845l150.691-61.717-30.603-74.723L326.9 728.816z"
|
||||
transform="rotate(46.353 540 540)"
|
||||
></path>
|
||||
<path
|
||||
d="M541.381 95.002C786.349 95.748 985 294.858 985 540S786.349 984.252 541.381 984.998v-99.296C731.547 884.957 885.705 730.339 885.705 540S731.547 195.043 541.381 194.298V95.002z"></path>
|
||||
<circle
|
||||
cx="708"
|
||||
cy="479"
|
||||
r="61"
|
||||
fill="#1F3E6B"
|
||||
transform="matrix(.7377 0 0 .7377 104.705 102.639)"
|
||||
></circle>
|
||||
<path
|
||||
fill="#1F3E6B"
|
||||
d="M627 363c51.328 0 93 41.672 93 93s-41.672 93-93 93-93-41.672-93-93 41.672-93 93-93zm0 34.428c32.327 0 58.572 26.245 58.572 58.572S659.327 514.572 627 514.572 568.428 488.327 568.428 456s26.245-58.572 58.572-58.572z"
|
||||
></path>
|
||||
<path
|
||||
fill="#1F3E6B"
|
||||
d="M534 564H376l39.5-158h79L534 564z"
|
||||
transform="matrix(-.48675 -.5974 .77525 -.63166 372.779 1129.97)"
|
||||
></path>
|
||||
<path
|
||||
fill="#1F3E6B"
|
||||
d="M406 553.837c38.127 0 69.081 30.955 69.081 69.082C475.081 661.046 444.127 692 406 692s-69.081-30.954-69.081-69.081 30.954-69.082 69.081-69.082zm0 17.271c28.595 0 51.811 23.215 51.811 51.811 0 28.595-23.216 51.811-51.811 51.811s-51.811-23.216-51.811-51.811c0-28.596 23.216-51.811 51.811-51.811z"
|
||||
transform="translate(19.825 13)"
|
||||
></path>
|
||||
<path
|
||||
fill="#1F3E6B"
|
||||
d="M296.936 630.919c-13.657-12.11-24.394-25.672-31.847-40.082H410v40.082H296.936z"
|
||||
transform="matrix(1.31489 .58767 -.82202 1.83925 393.401 -726.359)"
|
||||
></path>
|
||||
<circle
|
||||
cx="410"
|
||||
cy="630.919"
|
||||
r="40.081"
|
||||
fill="#1F3E6B"
|
||||
transform="translate(15.825 5)"
|
||||
></circle>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="_Radial1"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientTransform="matrix(540 0 0 540 540 540)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#2A91D7"></stop>
|
||||
<stop offset="1" stopColor="#071027"></stop>
|
||||
</radialGradient>
|
||||
<linearGradient
|
||||
id="_Linear3"
|
||||
x1="0"
|
||||
x2="1"
|
||||
y1="0"
|
||||
y2="0"
|
||||
gradientTransform="matrix(574 0 0 564 253 540)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor="#B75ECE"></stop>
|
||||
<stop offset="0.51" stopColor="#2F94F2" stopOpacity="0.7"></stop>
|
||||
<stop offset="1" stopColor="#00A6FF" stopOpacity="0.6"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SteamAndDeckLogo;
|
161
src/components/userComponents/CreateUser.tsx
Normal file
161
src/components/userComponents/CreateUser.tsx
Normal file
@ -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 (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl isInvalid={Boolean(errors.name)} mb={'3'}>
|
||||
<FormLabel htmlFor='name'>Full Name</FormLabel>
|
||||
<Input
|
||||
id='name'
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder='Full Name'
|
||||
{...register('name', { required: 'Name is required' })}
|
||||
/>
|
||||
{errors.name && <FormErrorMessage>{ errors.name.message?.toString()}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
<FormControl isInvalid={Boolean(errors.displayName)} mb={'3'}>
|
||||
<FormLabel htmlFor='displayName'>Display Name</FormLabel>
|
||||
<Input
|
||||
id='displayName'
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder='Display Name'
|
||||
{...register('displayName', { required: 'Display Name is required' })}
|
||||
/>
|
||||
{errors.displayName && <FormErrorMessage>{ errors.displayName.message?.toString()}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
<FormControl isInvalid={Boolean(errors.email)} mb={'3'}>
|
||||
<FormLabel htmlFor='email'>Email</FormLabel>
|
||||
<Input
|
||||
id='email'
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder='Email'
|
||||
{...register('email', { required: 'Email is required' })}
|
||||
/>
|
||||
{errors.email && <FormErrorMessage>{ errors.email.message?.toString()}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
<FormControl isInvalid={Boolean(errors.password)} mb={'3'}>
|
||||
<FormLabel htmlFor='password'>Password</FormLabel>
|
||||
<Input
|
||||
id='password'
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder='Password'
|
||||
type='password'
|
||||
{...register('password', {
|
||||
required: 'You must specify a password',
|
||||
minLength: {
|
||||
value: 8,
|
||||
message: 'Password must have at least 8 characters',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.password && <FormErrorMessage>{ errors.password.message?.toString()}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
{errors.password && (
|
||||
<>
|
||||
<Alert status='error' style={{ marginBottom: 10 }}>
|
||||
<AlertIcon />
|
||||
<AlertDescription>{errors.password.message?.toString()}</AlertDescription>
|
||||
{/*<CloseButton position='absolute' right='8px' top='8px' />*/}
|
||||
</Alert>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
<FormControl isInvalid={Boolean(errors.repeat_password)} mb={'3'}>
|
||||
<FormLabel htmlFor='repeat_password'>Repeat Password</FormLabel>
|
||||
<Input
|
||||
id='repeat_password'
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder='Repeat Password'
|
||||
type='password'
|
||||
{...register('repeat_password', {
|
||||
validate: (value) =>
|
||||
value === password.current || 'The passwords do not match',
|
||||
})}
|
||||
/>
|
||||
{errors.repeat_password && <FormErrorMessage>{ errors.repeat_password.message?.toString()}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
{errors.repeat_password && (
|
||||
<>
|
||||
<Alert status='error' style={{ marginBottom: 10 }}>
|
||||
<AlertIcon />
|
||||
<AlertDescription>{errors.repeat_password.message?.toString()}</AlertDescription>
|
||||
{/*<CloseButton position='absolute' right='8px' top='8px' />*/}
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
178
src/components/userComponents/EditUser.tsx
Normal file
178
src/components/userComponents/EditUser.tsx
Normal file
@ -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 (
|
||||
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
|
||||
<Heading as="h1" marginBottom={'3rem'}>Edit {user.displayName}</Heading>
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="name">Full Name</FormLabel>
|
||||
<Input
|
||||
id="name"
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
{...register('name', { required: 'Name is required' })}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl mt={'3'}>
|
||||
<FormLabel htmlFor="displayName">Display Name</FormLabel>
|
||||
<Input
|
||||
id="displayName"
|
||||
bg={bg}
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
{...register('displayName', { required: 'Display Name is required' })}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl mt={'3'}>
|
||||
<FormLabel htmlFor="email">Email</FormLabel>
|
||||
<Input
|
||||
id="email"
|
||||
bg={bg}
|
||||
_placeholder={{ color: 'gray.500' }}
|
||||
isDisabled
|
||||
border="1px"
|
||||
borderColor="gray.500"
|
||||
rounded="md"
|
||||
placeholder="Email"
|
||||
{...register('email', { required: 'Email is required' })}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Flex mt={'6'}>
|
||||
<FormLabel htmlFor={"theme"}>Dark Mode:</FormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="theme"
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<Flex mt={'6'}>
|
||||
<FormLabel htmlFor={"stickyNav"}>Sticky Navbar:</FormLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="stickyNav"
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
<br />
|
||||
<Stack spacing={[1, 5]} direction={'row'} align="self-end" pb="3">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isDirty}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={passwordLoading.get()}
|
||||
isDisabled={passwordDisabled.get()}
|
||||
onMouseDown={async () => {
|
||||
passwordLoading.set(true)
|
||||
passwordDisabled.set(true)
|
||||
await engageChangePassword({ email: user.email })
|
||||
passwordLoading.set(false)
|
||||
}}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
}
|
74
src/components/userComponents/UserProfile.tsx
Normal file
74
src/components/userComponents/UserProfile.tsx
Normal file
@ -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 (
|
||||
<Container maxW={'5xl'} my={'5rem'} centerContent>
|
||||
<Heading mb={'1rem'} as="h1">User Profile</Heading>
|
||||
<Grid templateColumns={`repeat(7, 1fr)`} templateRows={'2'} gap={6}>
|
||||
<GridItem rowStart={1} rowEnd={1} colStart={7} alignSelf={'end'} justifySelf={'end'}>
|
||||
<Button as={Link} to={`/edit-user`} colorScheme="blue" style={{textAlign: 'right'}}>
|
||||
<Text mt={'1'}>
|
||||
Edit
|
||||
</Text>
|
||||
</Button>
|
||||
</GridItem>
|
||||
<GridItem rowStart={2} rowEnd={2} colSpan={2} colStart={2}>
|
||||
<Box display={'flex'} alignItems={'center'}>
|
||||
<Image bg={bg} p="5" rounded="md" objectFit={'cover'} src={user.avatar}
|
||||
alt={`${user.displayName} Avatar`}/>
|
||||
</Box>
|
||||
</GridItem>
|
||||
<GridItem rowStart={2} rowEnd={2} colStart={4} colEnd={8}>
|
||||
<Box bg={bg} p="5" rounded="md">
|
||||
<TableContainer>
|
||||
<Table variant="simple">
|
||||
<Tbody>
|
||||
<Tr>
|
||||
<Td><Text fontSize={'1.5rem'}>Display Name: </Text></Td>
|
||||
<Td><Text fontSize={'1.5rem'}>{user.displayName}</Text></Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td><Text fontSize={'1.5rem'}>Full Name: </Text></Td>
|
||||
<Td><Text fontSize={'1.5rem'}>{user.name}</Text></Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td><Text fontSize={'1.5rem'}>Email: </Text></Td>
|
||||
<Td><Text fontSize={'1.5rem'}>{user.email}</Text></Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td><Text fontSize={'1.5rem'}>Role: </Text></Td>
|
||||
<Td><Text fontSize={'1.5rem'}>{user.role}</Text></Td>
|
||||
</Tr>
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfile
|
40
src/errors/NotFound.tsx
Normal file
40
src/errors/NotFound.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Heading as="h1" style={styles.retroFontHeader}>
|
||||
404 :(
|
||||
</Heading>
|
||||
<Heading
|
||||
as="h3"
|
||||
style={{fontSize: '2.5rem', marginBottom: '2rem', marginLeft: '5rem'}}
|
||||
>
|
||||
It looks like you hit a{' '}
|
||||
{<span style={styles.retroFontBody}>DEAD</span>} end!
|
||||
</Heading>
|
||||
<Button colorScheme='blue' as={Link} to={'/games'} style={{fontSize: '1.2rem', marginLeft: '5rem'}}>
|
||||
Back to Games List
|
||||
</Button>
|
||||
<Suspense fallback={<LoadingModal />}>
|
||||
<Video/>
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
42
src/errors/SomethingWentWrong.tsx
Normal file
42
src/errors/SomethingWentWrong.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Heading as="h1" style={styles.retroFontHeader}>
|
||||
Something Went Wrong
|
||||
</Heading>
|
||||
<Heading
|
||||
as="h3"
|
||||
style={{fontSize: '2.5rem', marginBottom: '2rem', marginLeft: '5rem'}}
|
||||
>
|
||||
{<span style={styles.retroFontBody}>WE APOLOGIZE!</span>}<br/>
|
||||
Please contact the website administrator!
|
||||
</Heading>
|
||||
<Button colorScheme='blue' as={Link} to={'/games'} style={{fontSize: '1.2rem', marginLeft: '5rem'}}>
|
||||
<div style={{marginTop: '.25rem'}}>
|
||||
Back to Games List
|
||||
</div>
|
||||
</Button>
|
||||
<Suspense fallback={<LoadingModal/>}>
|
||||
<Video/>
|
||||
</Suspense>
|
||||
</>
|
||||
)
|
||||
}
|
61
src/errors/TestError.tsx
Normal file
61
src/errors/TestError.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Container>
|
||||
<Heading as='h1'>Test Error component</Heading>
|
||||
<Container>
|
||||
<div>
|
||||
<Button onMouseDown={handleNotFound}>Not Found</Button>
|
||||
<Button onMouseDown={handleBadRequest}>Bad Request</Button>
|
||||
<Button onMouseDown={handleValidationError}>Validation Error</Button>
|
||||
<Button onMouseDown={handleServerError}>Server Error</Button>
|
||||
<Button onMouseDown={handleUnauthorised}>Unauthorised</Button>
|
||||
<Button onMouseDown={handleBadGuid}>Bad Guid</Button>
|
||||
</div>
|
||||
</Container>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
42
src/errors/Video.tsx
Normal file
42
src/errors/Video.tsx
Normal file
@ -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 (
|
||||
<div>
|
||||
{window.innerWidth >= 895 ? (
|
||||
<video
|
||||
className='videoBackground'
|
||||
poster={game_over_desktop}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
loop
|
||||
preload="auto"
|
||||
>
|
||||
<source src={game_over_mp4} type="video/mp4" />
|
||||
<source src={game_over_webm} type="video/webm" />
|
||||
Your browser does not support the video tag!
|
||||
</video>
|
||||
) : (
|
||||
<video
|
||||
className='videoBackground'
|
||||
poster={game_over_mobile}
|
||||
playsInline
|
||||
muted
|
||||
loop
|
||||
preload="auto"
|
||||
>
|
||||
<source src={game_over_mp4} type="video/mp4" />
|
||||
<source src={game_over_webm} type="video/webm" />
|
||||
Your browser does not support the video tag!
|
||||
</video>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
};
|
35
src/index.tsx
Normal file
35
src/index.tsx
Normal file
@ -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(
|
||||
<React.StrictMode>
|
||||
<Router history={history}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode}/>
|
||||
<App/>
|
||||
</ChakraProvider>
|
||||
</Router>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// 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();
|
54
src/models/defaultGame.ts
Normal file
54
src/models/defaultGame.ts
Normal file
@ -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
|
11
src/models/defaultUser.ts
Normal file
11
src/models/defaultUser.ts
Normal file
@ -0,0 +1,11 @@
|
||||
const defaultUser = {
|
||||
_id: '',
|
||||
name: '',
|
||||
displayName: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
token: '',
|
||||
role: '',
|
||||
}
|
||||
|
||||
export default defaultUser
|
100
src/models/game.ts
Normal file
100
src/models/game.ts
Normal file
@ -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
|
||||
}
|
378
src/models/tags.ts
Normal file
378
src/models/tags.ts
Normal file
@ -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" },
|
||||
];
|
||||
|
44
src/models/user.ts
Normal file
44
src/models/user.ts
Normal file
@ -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
|
||||
}
|
74
src/react-app-env.d.ts
vendored
Normal file
74
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
||||
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<React.SVGProps<
|
||||
SVGSVGElement
|
||||
> & { 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
|
||||
}
|
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal file
@ -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;
|
BIN
src/resources/fonts/retrogaming.ttf
Normal file
BIN
src/resources/fonts/retrogaming.ttf
Normal file
Binary file not shown.
BIN
src/resources/images/game_over.jpg
Normal file
BIN
src/resources/images/game_over.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 611 KiB |
BIN
src/resources/images/game_over_mobile.jpg
Normal file
BIN
src/resources/images/game_over_mobile.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 294 KiB |
BIN
src/resources/video/game_over.mp4
Normal file
BIN
src/resources/video/game_over.mp4
Normal file
Binary file not shown.
BIN
src/resources/video/game_over.webm
Normal file
BIN
src/resources/video/game_over.webm
Normal file
Binary file not shown.
5
src/setupTests.ts
Normal file
5
src/setupTests.ts
Normal file
@ -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';
|
37
src/stateManagement/gameState.ts
Normal file
37
src/stateManagement/gameState.ts
Normal file
@ -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)
|
15
src/stateManagement/loadingState.ts
Normal file
15
src/stateManagement/loadingState.ts
Normal file
@ -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<boolean>) => {
|
||||
loadingState.set(bool)
|
||||
loadingAndContinueState.set(bool)
|
||||
loadingAndSaveState.set(bool)
|
||||
}
|
4
src/stateManagement/ratingState.ts
Normal file
4
src/stateManagement/ratingState.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { hookstate } from '@hookstate/core'
|
||||
|
||||
export const ratingState = hookstate('[]')
|
||||
export const steamRatingState = hookstate('[]')
|
30
src/stateManagement/tagState.ts
Normal file
30
src/stateManagement/tagState.ts
Normal file
@ -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: '',
|
||||
},
|
||||
],
|
||||
})
|
103
src/stateManagement/userState.ts
Normal file
103
src/stateManagement/userState.ts
Normal file
@ -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)
|
91
src/styles/overrides.css
Normal file
91
src/styles/overrides.css
Normal file
@ -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;
|
||||
}
|
18
src/styles/theme.tsx
Normal file
18
src/styles/theme.tsx
Normal file
@ -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),
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
6
src/styles/typography.css
Normal file
6
src/styles/typography.css
Normal file
@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "RetroGaming";
|
||||
src: url("../resources/fonts/retrogaming.ttf");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
11
src/styles/videoBackground.css
Normal file
11
src/styles/videoBackground.css
Normal file
@ -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;
|
||||
}
|
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()]
|
||||
})
|
Loading…
Reference in New Issue
Block a user