├── .gitignore
├── README.md
├── firebase.json
├── functions
├── .gitignore
├── index.js
├── package-lock.json
└── package.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo.png
└── manifest.json
└── src
├── assets
├── dailyindiegame.png
├── fanatical.png
├── groupees.ico
├── humblebundle.png
├── im-in.jpg
├── images
│ ├── addNew.png
│ ├── export.png
│ ├── fieldSettings.png
│ ├── gameInfo-1.png
│ ├── gameInfo-2.png
│ ├── import-1.png
│ ├── import-2.png
│ ├── statistics.png
│ └── table.png
├── indiegala.ico
├── itad-logo.png
├── itad-logo.svg
├── logo.png
├── logo.svg
├── macgamestore.ico
├── userscripts
│ ├── keys-db-create-giveaway.meta.js
│ └── keys-db-create-giveaway.user.js
└── vids
│ ├── 2020-07-23_17-47-43.webm
│ ├── filtering.mp4
│ ├── gameinfo.mp4
│ ├── new-options.mp4
│ └── share-tutorial.mp4
├── components
├── App.js
├── ErrorBox
│ ├── ErrorBox.js
│ └── index.js
├── Header
│ ├── Header.js
│ └── index.js
├── KeysDbApp
│ ├── Cells
│ │ ├── ActionsCell
│ │ │ ├── ActionsCell.js
│ │ │ └── index.js
│ │ ├── AppIdCell
│ │ │ ├── AppIdCell.js
│ │ │ └── index.js
│ │ ├── DateCell
│ │ │ ├── DateCell.js
│ │ │ └── index.js
│ │ ├── HeaderCell
│ │ │ ├── HeaderCell.js
│ │ │ └── index.js
│ │ ├── KeyCell
│ │ │ ├── KeyCell.js
│ │ │ └── index.js
│ │ ├── NameCell
│ │ │ ├── NameCell.js
│ │ │ └── index.js
│ │ ├── NoteCell
│ │ │ ├── NoteCell.js
│ │ │ └── index.js
│ │ ├── OptionsCell
│ │ │ ├── OptionsCell.js
│ │ │ └── index.js
│ │ ├── SteamAchievementsCell
│ │ │ ├── SteamAchievementsCell.js
│ │ │ └── index.js
│ │ ├── SteamBundledCell
│ │ │ ├── SteamBundledCell.js
│ │ │ └── index.js
│ │ ├── SteamCardsCell
│ │ │ ├── SteamCardsCell.js
│ │ │ └── index.js
│ │ └── UrlCell
│ │ │ ├── UrlCell.js
│ │ │ └── index.js
│ ├── DataFilters
│ │ ├── DataFilters.js
│ │ └── index.js
│ ├── FieldSettings
│ │ ├── FieldSettings.js
│ │ └── index.js
│ ├── FilterDropdown
│ │ ├── FilterDropdown.js
│ │ └── index.js
│ ├── HeaderRow
│ │ ├── HeaderRow.js
│ │ └── index.js
│ ├── KeyRow
│ │ ├── KeyRow.js
│ │ └── index.js
│ ├── KeysTable
│ │ ├── KeysTable.js
│ │ └── index.js
│ ├── Modals
│ │ ├── ChangelogModal
│ │ │ ├── ChangelogModal.js
│ │ │ └── index.js
│ │ ├── CreateSteamgiftsGiveawayModal
│ │ │ ├── CreateSteamgiftsGiveawayModal.js
│ │ │ └── index.js
│ │ ├── GameInfoModal
│ │ │ ├── GameInfoModal.js
│ │ │ └── index.js
│ │ ├── ImportModal
│ │ │ ├── ImportModal.js
│ │ │ └── index.js
│ │ ├── NewModal
│ │ │ ├── NewModal.js
│ │ │ └── index.js
│ │ ├── SearchModal
│ │ │ ├── SearchModal.js
│ │ │ └── index.js
│ │ ├── SetColumnSettingsModal
│ │ │ ├── SetColumnSettingsModal.js
│ │ │ └── index.js
│ │ ├── ShareModal
│ │ │ ├── ShareModal.js
│ │ │ └── index.js
│ │ └── TableSettingsModal
│ │ │ ├── TableSettingsModal.js
│ │ │ └── index.js
│ ├── OptionsEditor
│ │ ├── OptionsEditor.js
│ │ └── index.js
│ ├── Settings
│ │ ├── Settings.js
│ │ └── index.js
│ └── SortDropdown
│ │ ├── SortDropdown.js
│ │ └── index.js
└── auth
│ ├── GoogleLoginComponent
│ ├── GoogleLoginComponent.js
│ └── index.js
│ ├── Login.js
│ ├── SteamLogin
│ ├── SteamLogin.js
│ └── index.js
│ └── SteamLoginComponent
│ ├── SteamLoginComponent.js
│ └── index.js
├── constants
├── spreadsheetConstants.js
├── statisticsConstants.js
├── steamConstants.js
└── tableConstants.js
├── firebase
├── context.js
├── firebase.js
└── index.js
├── hooks
├── formValidations
│ ├── validateHeaderSetting.js
│ ├── validateImport.js
│ ├── validateNewKey.js
│ ├── validateOption.js
│ ├── validateSettings.js
│ ├── validateSteamgiftsGiveaway.js
│ └── validateTableSettings.js
├── useBottomPage.js
├── useFormValidation.js
├── useGapi.js
├── useInterval.js
├── useLocalStorage.js
├── usePrevious.js
├── useRecharts.js
├── useSteam.js
├── useUrlParams.js
└── useWindowDimensions.js
├── index.js
├── lib
├── google
│ └── Spreadsheets.js
├── itad
│ └── ItadApi.js
└── steam
│ └── SteamApi.js
├── pages
├── ErrorPage
│ └── ErrorPage.js
├── Home
│ └── Home.js
├── KeysDBPage
│ └── KeysDBPage.js
├── PrivacyNoticePage
│ └── PrivacyNoticePage.js
├── SetupPage
│ └── SetupPage.js
├── StatisticsPage
│ └── StatisticsPage.js
└── TermsAndContitionsPage
│ └── TermsAndContitionsPage.js
├── serviceWorker.js
├── store
├── actionTypes
│ ├── AuthenticationActionTypes.js
│ ├── FilterActionTypes.js
│ ├── ImportActionTypes.js
│ ├── StatisticsActionsTypes.js
│ ├── TableActionTypes.js
│ └── ThemeActionTypes.js
├── actions
│ ├── AuthenticationActions.js
│ ├── FilterActions.js
│ ├── ImportActions.js
│ ├── StatisticsActions.js
│ ├── TableActions.js
│ └── ThemeActions.js
├── reducers
│ ├── AuthenticationReducer
│ │ ├── AuthenticationReducer.js
│ │ └── index.js
│ ├── FilterReducer
│ │ ├── FilterReducer.js
│ │ └── index.js
│ ├── ImportReducer
│ │ ├── ImportReducer.js
│ │ └── index.js
│ ├── StatisticsReducer
│ │ ├── StatisticsReducer.js
│ │ └── index.js
│ ├── TableReducer
│ │ ├── TableReducer.js
│ │ └── index.js
│ ├── ThemeReducer
│ │ ├── ThemeReducer.js
│ │ └── index.js
│ └── index.js
└── store.js
├── styles
└── index.css
└── utils
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Ignore Firebase Config
4 | .firebase
5 | .firebaserc
6 | /src/firebase/config.js
7 |
8 | # Ignore Google Config
9 | /src/Google/config.js
10 |
11 | # Ignore ITAD Config
12 | /src/lib/itad/config.js
13 |
14 | # Ignore Steam Config
15 | /src/lib/steam/config.js
16 |
17 | # dependencies
18 | /node_modules
19 | /.pnp
20 | .pnp.js
21 |
22 | # testing
23 | /coverage
24 |
25 | # production
26 | /build
27 |
28 | # misc
29 | .DS_Store
30 | .env.local
31 | .env.development.local
32 | .env.test.local
33 | .env.production.local
34 |
35 | npm-debug.log*
36 | yarn-debug.log*
37 | yarn-error.log*
38 | src/lib/google/config.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The TL:DR
2 | [KeysDB](https://keys-db.web.app/) is a completely **free** and **private** database for managing your collection of game keys, with features that appeal for regular gamers, traders and collectors. 👏
3 |
4 | # Introduction
5 |
6 | As a Gamer and a key hoarder I had gathered a lot of Steam\GOG\Origin\etc keys to keep,
7 | So I started saving it all into a google spreadsheet, from there I added a [GScript](https://github.com/yotamHak/Steam-Related/wiki/Google-Apps-Script) that collected some data about the key I added,
8 | Overtime the GScript turned into a chore to maintain so I decided to upgrade it a bit and add some more functionality to it,
9 | This is the result!
10 |
11 | # What's this then?
12 |
13 | I set out some goals when making this:
14 | 1. Privacy - I wanted privacy and safty as much as you can get, that's why I decided to go with Google Spreadsheets as the Database itself.
15 | 2. Functionality - I wanted to add more functionality compared to the GScript.
16 | 3. UI\UX - ReactJS is a proven JavaScript library and a well known and loved. (I enjoy using it and learning from it, so that's why I chose it).
17 |
18 | # Features
19 |
20 | When I started building it, I had some goals, but everytime I used it, I got more and more ideas and features I wanted to add,
21 | So this is still in-progress, but the main, working features are:
22 | 1. Easy key adding - Add a game with integrated additional data collection, like appid, related urls (itad, steam) and more...
23 | 2. Intuitive UI - Use filters to view your collection of keys.
24 | 3. Game information - View extra game information with bundle history and live bundles if available, screenshots\trailers\youtube gameplay, lowest recorded price, reviews, achievements and more
25 | 4. Dynamic Fields - Dynamically change fields from a selection of generic field types, and custom types as-well, easy options managments, select what's private, what's filterable and sortable and more. (I'm still adding more support and more fields)
26 | 5. Sharing - Are you a trader? well if you are, or you just want to show-off your collection, you can export your collection without the selected private fields (like the keys), and share it with whomever you want.
27 | 6. More features are coming!
28 |
29 | # Technologies
30 |
31 | [KeysDB](https://keys-db.web.app/) is stored on [Firebase](https://firebase.google.com/), and is a pure client app, there's no back-end, no database and no users.
32 |
33 | These are the primary technologies I'm using:
34 |
35 | [ReactJS](https://reactjs.org/)
36 |
37 | [React-Redux](https://react-redux.js.org/)
38 |
39 | [Semantic-UI](https://react.semantic-ui.com/)
40 |
41 | ### I'm also using API from
42 |
43 | [Steam](https://store.steampowered.com/)
44 |
45 | [IsThereAnyDeal](https://itad.docs.apiary.io/)
46 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "./build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "/api/**",
12 | "function": "api"
13 | },
14 | {
15 | "source": "/**",
16 | "destination": "/index.html"
17 | }
18 | ]
19 | }
20 | }
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const express = require('express');
3 | const https = require('https');
4 | const app = express();
5 | const cors = require('cors')({ origin: true });
6 | const admin = require('firebase-admin');
7 | const cheerio = require('cheerio');
8 |
9 | app.use(cors);
10 |
11 | admin.initializeApp({
12 | credential: admin.credential.applicationDefault()
13 | })
14 |
15 | // const { request } = require('http');
16 | // const httpGet = url => {
17 | // return new Promise((resolve, reject) => {
18 | // http.get(url, res => {
19 | // res.setEncoding('utf8');
20 | // let body = '';
21 | // res.on('data', chunk => body += chunk);
22 | // res.on('end', () => resolve(body));
23 | // }).on('error', reject);
24 | // });
25 | // };
26 |
27 | app.get('/api/appDetails', (request, response) => {
28 | response.set('Access-Control-Allow-Origin', "*");
29 | response.set('Cache-Control', 'public, max-age=300, s-maxage=600')
30 | cors(request, response, () => { });
31 |
32 | const appids = Number(request.query.appids);
33 | const url = `https://store.steampowered.com/api/appdetails/?appids=${appids}`
34 |
35 | https.get(url, res => {
36 | let body = '';
37 |
38 | res.on('data', function (chunk) {
39 | body += chunk;
40 | });
41 |
42 | res.on('end', function () {
43 | response.json({ status: "ok", result: JSON.parse(body)[appids] });
44 | });
45 |
46 | }).on('error', function (e) {
47 | console.log("Got error: " + e.message);
48 | })
49 | })
50 |
51 | app.get('/api/ownedGames', (request, response) => {
52 | response.set('Access-Control-Allow-Origin', "*");
53 | response.set('Cache-Control', 'public, max-age=300, s-maxage=600')
54 | cors(request, response, () => { });
55 |
56 | const apiKey = ""
57 | const steamId = ""
58 | const url = `https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=${apiKey}&include_played_free_games=1&include_appinfo=1&steamid=${steamId}&format=json`
59 | // const url = `http://store.steampowered.com/api/appdetails/?appids=57690`
60 |
61 | https.get(url, res => {
62 | let body = '';
63 |
64 | res.on('data', function (chunk) {
65 | body += chunk;
66 | });
67 |
68 | res.on('end', function () {
69 | response.json({ status: "ok", result: JSON.parse(body).response });
70 | });
71 |
72 | }).on('error', function (e) {
73 | console.log("Got error: " + e.message);
74 | })
75 | })
76 |
77 | app.get('/api/ownedGames-cached', (request, response) => {
78 | response.set('Cache-Control', 'public, max-age=300, s-maxage=600')
79 | response.send(`${Date.now()}`)
80 | })
81 |
82 | app.get('/api/removedGames', (request, response) => {
83 | response.set('Access-Control-Allow-Origin', "*");
84 | response.set('Cache-Control', 'public, max-age=300, s-maxage=600')
85 | cors(request, response, () => { });
86 |
87 | const url = `https://steam-tracker.com/apps/delisted`
88 |
89 | https.get(url, res => {
90 | let body = '';
91 |
92 | res.on('data', function (chunk) {
93 | body += chunk;
94 | });
95 |
96 | res.on('end', function () {
97 | const $ = cheerio.load(body)
98 | let games = {}
99 |
100 | $('#delisted-apps tbody tr')
101 | .each(function () {
102 | const rarity = $(this).children('td:nth-child(1)').children('.text-smaller').text().replace('(','').replace(')','').replace(' ','')
103 | const appid = $(this).attr('data-appid')
104 | const href = $(this).children('td:nth-child(2)').children('a').attr('href')
105 | const appname = $(this).children('td:nth-child(3)').text()
106 | const type = $(this).children('td:nth-child(4)').text()
107 |
108 | games[appid] = {
109 | "name": appname,
110 | "type": type,
111 | "steamdb": href,
112 | "rarity": rarity,
113 | }
114 | });
115 |
116 | response.json({ status: 200, data: { removedGames: games } });
117 | });
118 |
119 | }).on('error', function (e) {
120 | console.log("Got error: " + e.message);
121 | })
122 | })
123 |
124 | exports.api = functions.https.onRequest(app);
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "serve": "firebase serve --only functions",
6 | "shell": "firebase functions:shell",
7 | "start": "npm run shell",
8 | "deploy": "firebase deploy --only functions",
9 | "logs": "firebase functions:log"
10 | },
11 | "engines": {
12 | "node": "8"
13 | },
14 | "dependencies": {
15 | "cheerio": "^1.0.0-rc.3",
16 | "firebase-admin": "^8.13.0",
17 | "firebase-functions": "^3.3.0",
18 | "got": "^10.4.0"
19 | },
20 | "devDependencies": {
21 | "firebase-functions-test": "^0.1.6"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "keys-db",
3 | "version": "1.0.0",
4 | "description": "Keys-db using google sheets",
5 | "private": true,
6 | "dependencies": {
7 | "axios": "^0.18.0",
8 | "date-fns": "^1.30.1",
9 | "firebase": "^7.2.3",
10 | "firebase-admin": "^7.3.0",
11 | "firebase-functions": "^2.3.1",
12 | "gapi-script": "^1.0.2",
13 | "lodash": "^4.17.19",
14 | "moment": "^2.27.0",
15 | "react": "^16.13.1",
16 | "react-awesome-slider": "^4.1.0",
17 | "react-dom": "^16.13.1",
18 | "react-google-sheets": "^0.4.0",
19 | "react-redux": "^7.2.0",
20 | "react-router-dom": "^5.2.0",
21 | "react-scripts": "2.1.8",
22 | "recharts": "^1.8.5",
23 | "redux": "^4.0.5",
24 | "redux-devtools-extension": "^2.13.8",
25 | "semantic-ui-css": "^2.3.3",
26 | "semantic-ui-react": "^0.88.2",
27 | "uuid": "^8.3.0"
28 | },
29 | "scripts": {
30 | "start": "react-scripts start",
31 | "start-https": "set HTTPS=true&&react-scripts start",
32 | "build": "react-scripts build",
33 | "test": "react-scripts test",
34 | "eject": "react-scripts eject"
35 | },
36 | "keywords": [
37 | "react",
38 | "gdocs",
39 | "sheets",
40 | "steam"
41 | ],
42 | "author": "Yotam Hakim",
43 | "eslintConfig": {
44 | "extends": "react-app"
45 | },
46 | "browserslist": {
47 | "production": [
48 | ">0.2%",
49 | "not dead",
50 | "not op_mini all"
51 | ],
52 | "development": [
53 | "last 1 chrome version",
54 | "last 1 firefox version",
55 | "last 1 safari version"
56 | ]
57 | },
58 | "bugs": {
59 | "url": "https://github.com/yotamHak/key-db/issues"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
16 |
17 |
26 |
27 |
32 | Keys DB
33 |
34 |
35 |
36 | You need to enable JavaScript to run this app.
37 |
38 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/public/logo.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/dailyindiegame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/dailyindiegame.png
--------------------------------------------------------------------------------
/src/assets/fanatical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/fanatical.png
--------------------------------------------------------------------------------
/src/assets/groupees.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/groupees.ico
--------------------------------------------------------------------------------
/src/assets/humblebundle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/humblebundle.png
--------------------------------------------------------------------------------
/src/assets/im-in.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/im-in.jpg
--------------------------------------------------------------------------------
/src/assets/images/addNew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/addNew.png
--------------------------------------------------------------------------------
/src/assets/images/export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/export.png
--------------------------------------------------------------------------------
/src/assets/images/fieldSettings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/fieldSettings.png
--------------------------------------------------------------------------------
/src/assets/images/gameInfo-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/gameInfo-1.png
--------------------------------------------------------------------------------
/src/assets/images/gameInfo-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/gameInfo-2.png
--------------------------------------------------------------------------------
/src/assets/images/import-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/import-1.png
--------------------------------------------------------------------------------
/src/assets/images/import-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/import-2.png
--------------------------------------------------------------------------------
/src/assets/images/statistics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/statistics.png
--------------------------------------------------------------------------------
/src/assets/images/table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/images/table.png
--------------------------------------------------------------------------------
/src/assets/indiegala.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/indiegala.ico
--------------------------------------------------------------------------------
/src/assets/itad-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/itad-logo.png
--------------------------------------------------------------------------------
/src/assets/itad-logo.svg:
--------------------------------------------------------------------------------
1 | ITAD-rocket-color-bgBlack-RGB
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/macgamestore.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/macgamestore.ico
--------------------------------------------------------------------------------
/src/assets/userscripts/keys-db-create-giveaway.meta.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @version 0.22
3 | // ==/UserScript==
--------------------------------------------------------------------------------
/src/assets/userscripts/keys-db-create-giveaway.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Keys-DB Create Giveaway
3 | // @namespace https://github.com/yotamHak/keys-db
4 | // @version 0.22
5 | // @updateURL https://github.com/yotamHak/keys-db/raw/master/src/assets/userscripts/keys-db-create-giveaway.meta.js
6 | // @downloadURL https://github.com/yotamHak/keys-db/raw/master/src/assets/userscripts/keys-db-create-giveaway.user.js
7 | // @description Handles setting giveaway from keys-db
8 | // @author Keys-DB
9 | // @match https://www.steamgifts.com/giveaways/new*
10 | // @grant none
11 | // ==/UserScript==
12 |
13 | 'use strict';
14 |
15 | const params = getUrlParams();
16 | const form = $('.form__submit-button.js__submit-form').closest('form');
17 |
18 | function formatDate(date) {
19 | let formattedDate = $.datepicker.formatDate('M d, yy', date);
20 | const hours = date.getHours() % 12 < 10 ? '0' + date.getHours() % 12 : date.getHours() % 12;
21 | const minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
22 | const ampm = date.getHours() >= 12 ? 'pm' : 'am';
23 | formattedDate += " " + hours + ":" + minutes + " " + ampm;
24 |
25 | return formattedDate;
26 | }
27 |
28 | function getUrlParams() {
29 | const queryString = window.location.search;
30 | const urlParams = new URLSearchParams(queryString);
31 |
32 | return urlParams
33 | }
34 |
35 | function runEvent() {
36 | if (this.value) {
37 | this.element.val(this.value);
38 | }
39 |
40 | if (this.triggerEventName) {
41 | this.element.trigger(this.triggerEventName);
42 | }
43 |
44 | if (this.event) {
45 | $(document).off(this.event);
46 | }
47 | }
48 |
49 | $(document).on('ajaxSuccess.batch', (e, xhr, settings) => {
50 | if (settings.data.match(`${params.get('appid')}`)) {
51 | const result = JSON.parse(xhr.responseText).html.match(`${params.get('appid')}`);
52 |
53 | if (result) {
54 | const node = result.input.match(`data-autocomplete-id=\"(\\d+)\"`);
55 |
56 | if (node) {
57 | $(`[data-autocomplete-id=${node[1]}]`).click();
58 | }
59 | } else {
60 | console.log("Game not found, filling title...");
61 | $(document).trigger("fillTitle");
62 | }
63 |
64 | $(document).trigger("fillKey");
65 | }
66 | })
67 |
68 | $(document).ready(() => {
69 | params.forEach((param, paramKey) => {
70 | switch (paramKey) {
71 | case `appid`:
72 | runEvent.bind({
73 | "element": form.find(`input.js__autocomplete-name`),
74 | "value": param,
75 | "triggerEventName": "keyup",
76 | })();
77 | break;
78 | case `title`:
79 | $(document).on("fillTitle",
80 | runEvent.bind({
81 | "event": "fillTitle",
82 | "element": form.find(`input.js__autocomplete-name`),
83 | "value": param,
84 | "triggerEventName": "focus",
85 | }));
86 | break;
87 | case `key`:
88 | $(document).on("fillKey",
89 | runEvent.bind({
90 | "event": "fillKey",
91 | "element": form.find('textarea[name="key_string"]'),
92 | "value": param,
93 | "triggerEventName": "keyup",
94 | }));
95 | break;
96 | case `starting-time-offset`:
97 | runEvent.bind({
98 | "event": "fillStartingDateOffset",
99 | "element": form.find("input[name='start_time']"),
100 | "value": formatDate(new Date(new Date().getTime() + param * 60000))
101 | })();
102 | break;
103 | case `time-active`:
104 | runEvent.bind({
105 | "event": "fillEndingDate",
106 | "element": form.find("input[name='end_time']"),
107 | "value": formatDate(new Date(new Date().getTime() + param * 60000))
108 | })();
109 | break;
110 | default:
111 | break;
112 | }
113 | });
114 |
115 | if (params.values.length > 0) {
116 | runEvent.bind({
117 | "element": form.find(`[data-checkbox-value=key]`),
118 | "triggerEventName": "click",
119 | })();
120 | }
121 | })
--------------------------------------------------------------------------------
/src/assets/vids/2020-07-23_17-47-43.webm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/vids/2020-07-23_17-47-43.webm
--------------------------------------------------------------------------------
/src/assets/vids/filtering.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/vids/filtering.mp4
--------------------------------------------------------------------------------
/src/assets/vids/gameinfo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/vids/gameinfo.mp4
--------------------------------------------------------------------------------
/src/assets/vids/new-options.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/vids/new-options.mp4
--------------------------------------------------------------------------------
/src/assets/vids/share-tutorial.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yotamHak/keys-db/34d9bde2c367975526242754f095dbd7e7082214/src/assets/vids/share-tutorial.mp4
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { } from 'react';
2 | import { BrowserRouter, Switch, Route } from 'react-router-dom';
3 | import firebase, { FirebaseContext } from '../firebase';
4 |
5 | import Settings from './KeysDbApp/Settings/Settings';
6 | import Header from './Header/Header';
7 | import ErrorPage from '../pages/ErrorPage/ErrorPage';
8 | import Home from '../pages/Home/Home';
9 | import KeysDBPage from '../pages/KeysDBPage/KeysDBPage';
10 | import SetupPage from '../pages/SetupPage/SetupPage';
11 | import PrivacyNoticePage from '../pages/PrivacyNoticePage/PrivacyNoticePage';
12 | import TermsAndContitionsPage from '../pages/TermsAndContitionsPage/TermsAndContitionsPage';
13 | import StatisticsPage from '../pages/StatisticsPage/StatisticsPage';
14 |
15 | function App() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {/* */}
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default App;
39 |
--------------------------------------------------------------------------------
/src/components/ErrorBox/ErrorBox.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Message } from 'semantic-ui-react'
3 | import _ from 'lodash';
4 |
5 | const ErrorBox = ({ errors }) => (
6 |
7 | {
8 | !_.isEmpty(errors) && (
9 |
10 | Errors
11 |
12 | {
13 | Object.keys(errors).map((errorKey, index) => (
14 |
15 | {errors[errorKey]}
16 |
17 | ))
18 | }
19 |
20 |
21 | )
22 | }
23 |
24 | )
25 |
26 | export default ErrorBox
--------------------------------------------------------------------------------
/src/components/ErrorBox/index.js:
--------------------------------------------------------------------------------
1 | import ErrorBox from './ErrorBox';
2 |
3 | export default ErrorBox;
--------------------------------------------------------------------------------
/src/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import Header from './Header';
2 |
3 | export default Header;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/ActionsCell/index.js:
--------------------------------------------------------------------------------
1 | import ActionsCell from './ActionsCell';
2 |
3 | export default ActionsCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/AppIdCell/AppIdCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Icon } from "semantic-ui-react";
3 |
4 | function AppIdCell({ appId, rowIndex }) {
5 | return (
6 |
7 | {
8 | appId && (
9 |
10 |
15 |
16 | {appId}
17 |
18 | )
19 | }
20 |
21 | );
22 | }
23 |
24 | export default AppIdCell;
25 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/AppIdCell/index.js:
--------------------------------------------------------------------------------
1 | import AppIdCell from './AppIdCell';
2 |
3 | export default AppIdCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/DateCell/DateCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, } from "semantic-ui-react";
3 | import { parseSpreadsheetDate } from "../../../../utils";
4 |
5 |
6 | function DateCell({ dateAdded, rowIndex }) {
7 | // function handleChange() {
8 |
9 | // }
10 |
11 | return (
12 |
13 | {parseSpreadsheetDate(dateAdded, true)}
14 |
15 | )
16 |
17 | // return (
18 | //
19 | //
25 | //
26 | // )
27 | }
28 |
29 |
30 | export default DateCell;
31 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/DateCell/index.js:
--------------------------------------------------------------------------------
1 | import DateCell from './DateCell';
2 |
3 | export default DateCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/HeaderCell/HeaderCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Grid, } from "semantic-ui-react";
3 | import { useSelector } from "react-redux";
4 |
5 | function HeaderCell({ title }) {
6 | const headers = useSelector((state) => state.table.headers)
7 |
8 | return (
9 |
10 |
11 |
12 |
13 | {(headers[title] && headers[title].label) || title}
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default HeaderCell;
22 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/HeaderCell/index.js:
--------------------------------------------------------------------------------
1 | import HeaderCell from './HeaderCell';
2 |
3 | export default HeaderCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/KeyCell/index.js:
--------------------------------------------------------------------------------
1 | import KeyCell from './KeyCell';
2 |
3 | export default KeyCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/NameCell/NameCell.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Table, Popup, Header, Grid, Image } from "semantic-ui-react";
3 | import { useSelector, } from "react-redux";
4 |
5 | import { getValueByType, } from "../../../../utils";
6 | import GameInfoModal from "../../Modals/GameInfoModal";
7 |
8 | function NameCell({ name, rowIndex }) {
9 | const headers = useSelector((state) => state.table.headers)
10 | const gameData = useSelector((state) => state.table.rows[rowIndex])
11 |
12 | const [steamAppId, setSteamAppId] = useState(null)
13 | const [steamTitle, setSteamTitle] = useState(null)
14 |
15 | useEffect(() => {
16 | setSteamAppId(getValueByType(gameData, headers, "steam_appid"))
17 | setSteamTitle(getValueByType(gameData, headers, "steam_title"))
18 | }, [headers])
19 |
20 | return (
21 |
22 | {
23 | steamAppId && steamTitle
24 | ? (
25 |
30 |
31 | {name}}
37 | wide
38 | >
39 |
40 | {/* subheader={data.title} */}
41 |
42 |
43 |
44 |
45 |
46 | }
47 | />
48 | )
49 | : (
50 | {name}
51 | )
52 | }
53 |
54 | );
55 | }
56 |
57 | export default NameCell;
58 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/NameCell/index.js:
--------------------------------------------------------------------------------
1 | import NameCell from './NameCell';
2 |
3 | export default NameCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/NoteCell/NoteCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Popup, Icon } from "semantic-ui-react";
3 |
4 | function NoteCell({ note, rowIndex }) {
5 | return (
6 |
7 | {
8 | note && note !== '' && (
9 | }
11 | content={note}
12 | position='bottom center'
13 | />
14 | )
15 | }
16 |
17 | );
18 | }
19 |
20 | export default NoteCell;
21 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/NoteCell/index.js:
--------------------------------------------------------------------------------
1 | import NoteCell from './NoteCell';
2 |
3 | export default NoteCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/OptionsCell/OptionsCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Table, Dropdown } from "semantic-ui-react";
4 | import _ from 'lodash';
5 |
6 | import { parseOptions, hasWritePermission, getIndexById } from "../../../../utils";
7 | import { setNewRowChange } from "../../../../store/actions/TableActions";
8 |
9 | function OptionsCell({ rowIndex, title, header, onChange }) {
10 | const headers = useSelector((state) => state.table.headers)
11 | const gameData = useSelector((state) => state.table.rows[rowIndex])
12 |
13 | const dispatch = useDispatch();
14 |
15 | const options = parseOptions(header.options);
16 | const permission = useSelector((state) => state.authentication.permission)
17 | const [currentlySelected, setCurrentlySelected] = React.useState(options.filter(option => option.text === title)[0] || 0);
18 |
19 | function handleChange(e, { value }) {
20 | const selectedValue = options.filter(option => option.value === value)
21 | const isNew = selectedValue.length > 0 ? true : false
22 |
23 | if (!isNew) {
24 | // New value to add
25 | console.log("New value to add")
26 | } else {
27 | const changedValue = selectedValue[0].text;
28 | setCurrentlySelected(selectedValue[0]);
29 | // onChange(header, changedValue);
30 | dispatch(setNewRowChange(rowIndex, {
31 | ...gameData,
32 | [getIndexById(header.id, headers)]: changedValue
33 | }))
34 | }
35 | }
36 |
37 | return (
38 |
39 | (
45 | _.concat(
46 | result,
47 | option.color
48 | ? [{
49 | ...option,
50 | label: { color: option.color, empty: true, circular: true }
51 | }]
52 | : [{
53 | ...option
54 | }]
55 | )
56 | ), [])}
57 | />
58 |
59 | );
60 | }
61 |
62 | export default OptionsCell;
63 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/OptionsCell/index.js:
--------------------------------------------------------------------------------
1 | import OptionsCell from './OptionsCell';
2 |
3 | export default OptionsCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamAchievementsCell/SteamAchievementsCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Icon } from "semantic-ui-react";
3 | import { useSelector } from "react-redux";
4 |
5 | import { getValueById } from "../../../../utils"
6 |
7 | function SteamAchievementsCell({ value, rowIndex }) {
8 | const headers = useSelector((state) => state.table.headers)
9 | const gameData = useSelector((state) => state.table.rows[rowIndex])
10 |
11 | return (
12 |
13 | {
14 | value === 'Have'
15 | ?
16 | :
17 | }
18 |
19 | );
20 | }
21 |
22 | export default SteamAchievementsCell;
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamAchievementsCell/index.js:
--------------------------------------------------------------------------------
1 | import SteamAchievementsCell from './SteamAchievementsCell';
2 |
3 | export default SteamAchievementsCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamBundledCell/SteamBundledCell.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Table, Statistic, } from "semantic-ui-react";
3 | import { useSelector } from "react-redux";
4 | import _ from 'lodash';
5 |
6 | import { getValueByType } from "../../../../utils";
7 |
8 | import ItadApi from "../../../../lib/itad/ItadApi";
9 |
10 | function SteamBundledCell({ value, rowIndex, }) {
11 | const headers = useSelector((state) => state.table.headers)
12 | const gameData = useSelector((state) => state.table.rows[rowIndex])
13 | const itadMap = useSelector((state) => state.authentication.itad.map)
14 |
15 | const [steamTitle, setSteamTitle] = useState(null)
16 | const [steamAppId, setSteamAppId] = useState(null)
17 |
18 | const [itadPlainTitle, setItadPlainTitle] = useState(null)
19 |
20 | useEffect(() => {
21 | setSteamAppId(getValueByType(gameData, headers, "steam_appid"))
22 | setSteamTitle(getValueByType(gameData, headers, "steam_title"))
23 |
24 | steamTitle && steamAppId && itadMap && setItadPlainTitle(ItadApi.GetPlainName(itadMap, steamAppId))
25 | }, [headers, steamTitle,])
26 |
27 | return (
28 |
29 | {
30 | value !== null && _.parseInt(value) >= 0
31 | ? itadPlainTitle
32 | ? (
33 |
34 |
38 | {value}
39 | Times
40 |
41 |
42 | )
43 | :
47 | {value}
48 | Times
49 |
50 | :
51 | N\A
52 |
53 | }
54 |
55 | );
56 | }
57 |
58 | export default SteamBundledCell;
59 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamBundledCell/index.js:
--------------------------------------------------------------------------------
1 | import SteamBundledCell from './SteamBundledCell';
2 |
3 | export default SteamBundledCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamCardsCell/SteamCardsCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Icon } from "semantic-ui-react";
3 | import { useSelector } from "react-redux";
4 | import { getValueById } from "../../../../utils"
5 |
6 | function SteamCardsCell({ value, rowIndex, }) {
7 | const headers = useSelector((state) => state.table.headers)
8 | const gameData = useSelector((state) => state.table.rows[rowIndex])
9 |
10 | return (
11 |
12 | {
13 | value === 'Have'
14 | ?
15 | :
16 | }
17 |
18 | );
19 | }
20 |
21 | export default SteamCardsCell;
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/SteamCardsCell/index.js:
--------------------------------------------------------------------------------
1 | import SteamCardsCell from './SteamCardsCell';
2 |
3 | export default SteamCardsCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/UrlCell/UrlCell.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Table, Grid, Icon } from "semantic-ui-react";
3 |
4 | function UrlCell({ urls, rowIndex }) {
5 | return (
6 |
7 |
8 |
9 | {
10 | urls.map((url, index) => url.url && (
11 |
20 | ))
21 | }
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default UrlCell;
29 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Cells/UrlCell/index.js:
--------------------------------------------------------------------------------
1 | import UrlCell from './UrlCell';
2 |
3 | export default UrlCell;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/DataFilters/DataFilters.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { List, Label, Icon } from 'semantic-ui-react';
3 | import { useSelector, useDispatch } from "react-redux";
4 |
5 | import { removeFilter } from "../../../store/actions/FilterActions";
6 |
7 | function DataFilters() {
8 | const dispatch = useDispatch()
9 | const filters = useSelector((state) => state.filters)
10 |
11 | return (
12 |
13 | {
14 | filters.length > 0
15 | ? (
16 |
17 | {/* Filters: */}
18 |
24 |
25 | {
26 | filters.map((filter, filterIndex) => {
27 | return (
28 |
29 |
30 | {filter.key}
31 | {
32 | filter.values.map((filterValue, valueIndex) => (
33 | { dispatch(removeFilter({ key: filter.key, value: filterValue, id: filter.id })) }}
37 | key={valueIndex}>
38 | {filterValue}
39 |
40 |
41 | ))
42 | }
43 |
44 |
45 | )
46 | })
47 | }
48 |
49 |
50 | )
51 | : (
52 |
53 | Unfiltered
54 |
55 | )
56 | }
57 |
58 | );
59 | }
60 |
61 | export default DataFilters;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/DataFilters/index.js:
--------------------------------------------------------------------------------
1 | import DataFilters from './DataFilters';
2 |
3 | export default DataFilters;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/FieldSettings/FieldSettings.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Form, Input, Grid, Popup, Icon, Checkbox, } from 'semantic-ui-react';
3 |
4 | import { isDropdownType, getAllFieldTypes } from '../../../utils';
5 | import ErrorBox from '../../ErrorBox';
6 | import OptionsEditor from '../OptionsEditor';
7 |
8 | function FieldSettings({ headerKey, values, errors, handleChange, }) {
9 | function handleInitOptions(headerKey, values, allowEdit) {
10 | handleChange(null, {
11 | name: 'options',
12 | value: {
13 | allowEdit: allowEdit,
14 | values: values || [],
15 | }
16 | },
17 | headerKey ? headerKey : undefined
18 | )
19 | }
20 |
21 | function handleOptionsChange(newValues, headerKey) {
22 | handleChange(null, {
23 | name: 'options',
24 | value: newValues
25 | },
26 | headerKey ? headerKey : undefined
27 | )
28 | }
29 |
30 | function handleChangeWrapper(event, data) {
31 | handleChange(event,
32 | data,
33 | headerKey ? headerKey : undefined
34 | )
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
43 |
44 | }
46 | content='This represents the column header label.'
47 | position='right center'
48 | /> Label
49 |
50 |
56 |
57 |
58 |
61 | }
63 | content='Checking this will make this a private column, meaning, when you export, these columns will be removed from the newly spreadsheet.'
64 | position='right center'
65 | /> Private
66 |
67 | }
68 | checked={values['isPrivate']}
69 | name={'isPrivate'}
70 | onChange={handleChangeWrapper}
71 | />
72 |
73 |
74 |
77 | }
79 | content='Checking this will make this show up on the table.'
80 | position='right center'
81 | /> Display
82 |
83 | }
84 | checked={values['display']}
85 | name={'display'}
86 | onChange={handleChangeWrapper}
87 | />
88 |
89 |
90 |
93 | }
95 | content='Checking this will allow the user to apply a filter using the options, this only works for multi-options types.'
96 | position='right center'
97 | /> Filterable
98 |
99 | }
100 | checked={values['isFilter']}
101 | name={'isFilter'}
102 | onChange={handleChangeWrapper}
103 | />
104 |
105 |
106 |
109 | }
111 | content='Checking this will allow the user to apply sort on this field.'
112 | position='right center'
113 | /> Sortable
114 |
115 | }
116 | checked={values['sortable']}
117 | name={'sortable'}
118 | onChange={handleChangeWrapper}
119 | />
120 |
121 |
122 |
123 |
124 |
125 | }
127 | content='This represents the value type of the data that will be inputted in this specific column.'
128 | position='right center'
129 | /> Type
130 |
131 |
137 |
138 | {
139 |
140 | isDropdownType(values["type"]) &&
147 | }
148 |
149 |
150 |
151 |
152 |
153 | )
154 | }
155 |
156 | export default FieldSettings;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/FieldSettings/index.js:
--------------------------------------------------------------------------------
1 | import FieldSettings from './FieldSettings';
2 |
3 | export default FieldSettings;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/FilterDropdown/FilterDropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Header, Dropdown, } from "semantic-ui-react";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { resetTableParams } from "../../../store/actions/TableActions";
5 | import { addFilter } from "../../../store/actions/FilterActions";
6 |
7 | function FilterDropdown() {
8 | const headers = useSelector((state) => state.table.headers)
9 | const filters = useSelector((state) => state.filters)
10 |
11 | const dispatch = useDispatch()
12 |
13 | function filter(header, text) {
14 | const filter = filters.filter(filter => filter.key === header);
15 | const newFilter = filter.length > 0
16 | ? filter[0]
17 | : { key: header, values: [] }
18 |
19 | dispatch(resetTableParams(['offset']))
20 | dispatch(addFilter({
21 | key: header,
22 | values: newFilter.values.concat(text),
23 | id: headers[header].id
24 | }))
25 | }
26 |
27 | return (
28 |
29 |
30 |
31 |
32 | {
33 | Object.keys(headers)
34 | .filter(header => header !== "ID" && headers[header].isFilter)
35 | .map((key, index) => (
36 |
37 |
38 |
39 | {
40 | headers[key].options && headers[key].options.values && headers[key].options.values
41 | .filter(value => {
42 | const filter = filters.filter(filter => filter.key === key);
43 | return filter.length === 0 || filter[0].values.indexOf(value.value) === -1
44 | })
45 | .map((value, index) => filter(key, value.value)} key={index}>
46 | {value.value}
47 | )
48 | }
49 |
50 |
51 |
52 | ))
53 | }
54 |
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export default FilterDropdown;
62 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/FilterDropdown/index.js:
--------------------------------------------------------------------------------
1 | import FilterDropdown from './FilterDropdown';
2 |
3 | export default FilterDropdown;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/HeaderRow/HeaderRow.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import { Table, } from 'semantic-ui-react';
4 | import _ from 'lodash';
5 |
6 | import HeaderCell from "../Cells/HeaderCell";
7 |
8 | import { isUrlType, shouldAddField, } from "../../../utils";
9 |
10 | function HeaderRow() {
11 | // const rowChanges = useSelector((state) => state.table.changes)
12 | const headers = useSelector((state) => state.table.headers)
13 |
14 | const headersToDisplay = Object.keys(headers).reduce((result, headerKey, index) => {
15 | if (shouldAddField(headers, null, headers[headerKey].id)) {
16 | return _.concat(result, [isUrlType(headers[headerKey].type) ? "URLs" : headerKey])
17 | } else {
18 | return result
19 | }
20 | }, [])
21 |
22 | return (
23 |
24 |
25 | {
26 | headersToDisplay.map((headerKey, index) => {
27 | return
31 | })
32 | }
33 |
34 | );
35 | }
36 |
37 | export default HeaderRow;
38 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/HeaderRow/index.js:
--------------------------------------------------------------------------------
1 | import HeaderRow from './HeaderRow';
2 |
3 | export default HeaderRow;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/KeyRow/KeyRow.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Table, Button, } from 'semantic-ui-react';
4 | import _ from 'lodash';
5 |
6 | import KeyCell from "../Cells/KeyCell";
7 | import DateCell from "../Cells/DateCell";
8 | import NoteCell from "../Cells/NoteCell";
9 | import UrlCell from "../Cells/UrlCell";
10 | import AppIdCell from "../Cells/AppIdCell";
11 | import NameCell from "../Cells/NameCell";
12 | import OptionsCell from "../Cells/OptionsCell";
13 | import ActionsCell from "../Cells/ActionsCell";
14 | import SteamCardsCell from "../Cells/SteamCardsCell";
15 | import SteamAchievementsCell from "../Cells/SteamAchievementsCell";
16 | import SteamBundledCell from "../Cells/SteamBundledCell";
17 |
18 | import { getUrlsLocationAndValue, isDropdownType, getIndexById, isDateType, shouldAddField, isUrlType } from "../../../utils";
19 | import Spreadsheets from "../../../lib/google/Spreadsheets";
20 | import { removeNewRowChange, updateRow } from "../../../store/actions/TableActions";
21 | import { resetStatisticsStorage } from "../../../store/actions/StatisticsActions";
22 |
23 | function KeyRow({ rowIndex }) {
24 | const [hasChanges, setHasChanges] = useState(false);
25 | const [isSaving, setIsSaving] = useState(false);
26 |
27 | const spreadsheetId = useSelector((state) => state.authentication.currentSpreadsheetId)
28 | const sheetId = useSelector((state) => state.authentication.currentSheetId)
29 | const headers = useSelector((state) => state.table.headers)
30 | const gameData = useSelector((state) => state.table.rows[rowIndex])
31 | const rowChanges = useSelector((state) => state.table.changes[rowIndex])
32 |
33 | const urlsInGameData = getUrlsLocationAndValue(headers, gameData);
34 | const dispatch = useDispatch();
35 |
36 | useEffect(() => {
37 | rowChanges && setHasChanges(true)
38 | }, [rowChanges])
39 |
40 | function changeCallback(header, changedValue) {
41 | gameData[getIndexById(header.id, headers)] = changedValue;
42 | setHasChanges(true)
43 | }
44 |
45 | function selectCell(index, header, gameHeaderValue) {
46 | if (!shouldAddField(headers, gameData, header.id)) return
47 |
48 | const rKey = `${rowIndex}-${header.id}-${gameHeaderValue}`;
49 |
50 | if (header.type === 'steam_cards') {
51 | return
57 | } else if (header.type === 'steam_achievements') {
58 | return
64 | } else if (header.type === 'steam_bundled') {
65 | return
70 | } else if (header.type === 'steam_title') {
71 | return
78 | } else if (header.type === 'steam_key') {
79 | return
86 | } else if (header.type === 'steam_appid') {
87 | return
94 | } else if (header.type === 'key') {
95 | return
102 | } else if (header.type === 'text') {
103 | return
110 | } else if (isDropdownType(header.type)) {
111 | return
118 | } else if (isDateType(header.type)) {
119 | return
126 | } else if (isUrlType(header.type)) {
127 | return
132 | } else {
133 | return
138 | {gameHeaderValue}
139 |
140 | }
141 | }
142 |
143 | function saveChanges(rowIndex, rowChanges) {
144 | setIsSaving(true)
145 |
146 | Spreadsheets.Update(spreadsheetId, sheetId, rowChanges, rowChanges[0])
147 | .then(response => {
148 | dispatch(resetStatisticsStorage())
149 | dispatch(updateRow(rowIndex, rowChanges))
150 | dispatch(removeNewRowChange(rowIndex))
151 | setHasChanges(false)
152 | })
153 | .finally(() => {
154 | setIsSaving(false)
155 | })
156 | }
157 |
158 | return (
159 |
160 | {
161 | hasChanges
162 | ?
163 | { saveChanges(rowIndex, _.toArray(rowChanges)) }} loading={isSaving} circular basic size='mini' />
164 |
165 | :
166 | }
167 |
168 | {
169 | gameData && Object.keys(headers).map((key, index) => { return selectCell(index, headers[key], (rowChanges && rowChanges[index]) || gameData[index]) })
170 | }
171 |
172 | );
173 | }
174 |
175 | export default KeyRow;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/KeyRow/index.js:
--------------------------------------------------------------------------------
1 | import KeyRow from './KeyRow';
2 |
3 | export default KeyRow;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/KeysTable/index.js:
--------------------------------------------------------------------------------
1 | import KeysTable from './KeysTable';
2 |
3 | export default KeysTable;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/ChangelogModal/ChangelogModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal, Icon, List, Segment, Header, Container, } from "semantic-ui-react";
3 |
4 | export const changelog = [
5 | {
6 | version: '0.7.2',
7 | date: '24/10/2020',
8 | list: [
9 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
10 | ]
11 | },
12 | {
13 | version: '0.7.1',
14 | date: '21/10/2020',
15 | list: [
16 | 'Statistics Page - Removed empty categories from charts, Added better labels to charts, charts are now responsive.',
17 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
18 | ]
19 | },
20 | {
21 | version: '0.7',
22 | date: '17/10/2020',
23 | list: [
24 | 'Statistics Page - A brand new page showing your keys collection statistics.',
25 | ]
26 | },
27 | {
28 | version: '0.6.2',
29 | date: '14/10/2020',
30 | list: [
31 | 'New\\Edit Key - Key platform is now integrated to the key input.',
32 | ]
33 | },
34 | {
35 | version: '0.6.1',
36 | date: '13/10/2020',
37 | list: [
38 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
39 | ]
40 | },
41 | {
42 | version: '0.6',
43 | date: '12/10/2020',
44 | list: [
45 | 'Fields - New Key Platform field, Field is a multi-select field, and will display under the key field.',
46 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
47 | ]
48 | },
49 | {
50 | version: '0.5.9',
51 | date: '8/10/2020',
52 | list: [
53 | 'Table - New Create SteamGift giveaway feature, can be found in the actions dropdown.',
54 | ]
55 | },
56 | {
57 | version: '0.5.8.1',
58 | date: '4/10/2020',
59 | list: [
60 | 'Steam Login - fixed small issue.',
61 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
62 | ]
63 | },
64 | {
65 | version: '0.5.8',
66 | date: '29/9/2020',
67 | list: [
68 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
69 | 'Error handling - Better feedback when errors occur.',
70 | ]
71 | },
72 | {
73 | version: '0.5.7.1',
74 | date: '25/8/2020',
75 | list: [
76 | 'Table - Moved filter from table headers into it\'s own dropdown, near sorting.',
77 | 'Table - Some styling changes.',
78 | ]
79 | },
80 | {
81 | version: '0.5.7',
82 | date: '23/8/2020',
83 | list: [
84 | 'Table - You can now update ITAD information from the actions dropdown.',
85 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
86 | ]
87 | },
88 | {
89 | version: '0.5.6',
90 | date: '20/8/2020',
91 | list: [
92 | 'Table - Hovering for at least 1 second on game title will show popover of the games title and the games image from Steam.',
93 | 'Fields - New Steam Bundled field, will be filled automatically.',
94 | ]
95 | },
96 | {
97 | version: '0.5.5',
98 | date: '19/8/2020',
99 | list: [
100 | 'Settings - You can now add and delete fields.',
101 | 'Fields - New Steam Achievements field, will be filled automatically.',
102 | 'Note Field - Fixed issue where the note i still displayed even though it\'s empty.',
103 | 'General - Bugs, stability, performance fixes, and other boring stuff.',
104 | ]
105 | },
106 | {
107 | version: '0.5.4.1',
108 | date: '12/8/2020',
109 | list: [
110 | 'Game Info - Changed loading to give a better feedback.',
111 | 'Home - Grammatical fixes.',
112 | ]
113 | },
114 | {
115 | version: '0.5.4',
116 | date: '11/8/2020',
117 | list: [
118 | 'New\\Edit Key - Date have a date picker.',
119 | 'New\\Edit Key - Fixed issue with initial date.',
120 | 'New\\Edit Key - Fixed issue with selection inputs.',
121 | 'Home - Redesign to reflect features and grammatical fixes.',
122 | ]
123 | },
124 | {
125 | version: '0.5.3',
126 | date: '10/8/2020',
127 | list: [
128 | 'Added TOS and Privacy Policy to comply with Google OAuth verification request.',
129 | ]
130 | },
131 | {
132 | version: '0.5.2',
133 | date: '9/8/2020',
134 | list: [
135 | 'Import is now available.',
136 | 'Settings - Better explanation to each setting.',
137 | 'Options - Fixed editing issues.',
138 | 'Options - Pre-set options for certain types.',
139 | ]
140 | },
141 | {
142 | version: '0.5.1',
143 | date: '2/8/2020',
144 | list: [
145 | 'Options - Options are now editable.',
146 | ]
147 | },
148 | {
149 | version: '0.5',
150 | date: '1/8/2020',
151 | list: [
152 | 'Game Info - Game categories fix.',
153 | 'New\\Edit Key - Changed the minimum characters needed to add a game from 3 to 1.',
154 | 'Export - Export fix.',
155 | 'New\\Edit Key - Date value will be filled to today\'s date on open.',
156 | 'Changelog added.'
157 | ]
158 | },
159 | ]
160 |
161 | function ChangelogModal({ trigger }) {
162 | return (
163 | }
166 | centered={false}
167 | size={'small'}
168 | >
169 | Changelog
170 |
171 |
172 |
173 | {
174 | changelog.map((changelist, index) => (
175 |
176 |
177 |
178 | {
179 | changelist.list.map((listitem, index) => (
180 | {listitem}
181 | ))
182 | }
183 |
184 |
185 | ))
186 | }
187 |
188 |
189 |
190 |
191 | );
192 | }
193 |
194 | export default ChangelogModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/ChangelogModal/index.js:
--------------------------------------------------------------------------------
1 | import ChangelogModal from './ChangelogModal';
2 |
3 | export default ChangelogModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/CreateSteamgiftsGiveawayModal/CreateSteamgiftsGiveawayModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState, } from "react";
2 | import { useSelector } from "react-redux";
3 | import { Modal, Icon, Dropdown, Button, Form, Segment, Input, Message } from "semantic-ui-react";
4 |
5 | import validateSteamgiftsGiveaway from "../../../../hooks/formValidations/validateSteamgiftsGiveaway";
6 | import useFormValidation from "../../../../hooks/useFormValidation";
7 | import { getValueByType } from "../../../../utils";
8 | import ErrorBox from "../../../ErrorBox";
9 |
10 | const DEFAULT_STARTING_TIME_OFFSET = 15;
11 | const DEFAULT_GIVEAWAY_TIME_ACTIVE = 75
12 |
13 | function CreateSteamgiftsGiveawayModal({ rowIndex, trigger = }) {
14 | const headers = useSelector((state) => state.table.headers)
15 | const gameData = useSelector((state) => state.table.rows[rowIndex])
16 | const [modalOpen, setModalOpen] = useState(false)
17 |
18 | const handleOpen = () => setModalOpen(true)
19 | const handleClose = () => setModalOpen(false)
20 |
21 | const Child = React.Children.only(trigger);
22 | const newChildren = React.cloneElement(Child, { onClick: handleOpen });
23 |
24 | const INITIAL_STATE = {
25 | appid: getValueByType(gameData, headers, "steam_appid"),
26 | title: getValueByType(gameData, headers, "steam_title"),
27 | key: getValueByType(gameData, headers, "key"),
28 | startingTimeOffset: DEFAULT_STARTING_TIME_OFFSET,
29 | timeActive: DEFAULT_GIVEAWAY_TIME_ACTIVE,
30 | }
31 |
32 | const { handleSubmit, handleChange, values, errors, } = useFormValidation(INITIAL_STATE, validateSteamgiftsGiveaway, handleRedirect);
33 |
34 | function handleRedirect() {
35 | let formElement = document.createElement('a');
36 | formElement.setAttribute('id', 'go-to-steamgifts')
37 | formElement.setAttribute('href', `https://www.steamgifts.com/giveaways/new?appid=${values.appid}&title=${values.title}&key=${values.key}&starting-time-offset=${values.startingTimeOffset}&time-active=${values.timeActive}`)
38 | formElement.setAttribute('target', '_blank')
39 | formElement.setAttribute('rel', 'noopener noreferrer')
40 |
41 | document.getElementById('create-steamgift-giveaway-modal').append(formElement)
42 |
43 | formElement.click()
44 | formElement.remove()
45 | handleClose()
46 | }
47 |
48 | return (
49 | }
57 | >
58 | Create Giveaway
59 |
60 |
61 |
62 | Info
63 |
64 | To use this feature, you need to install Tampermonkey , Violentmonkey , Greasemonkey or some other userscript manager first.
65 | After installing a userscript manager, click here and you should be prompted to install the SteamGifts userscript.
66 |
67 |
68 |
142 |
143 |
144 |
145 |
146 |
147 | Cancel
148 | Create Giveaway
149 |
150 |
151 | );
152 | };
153 |
154 | export default CreateSteamgiftsGiveawayModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/CreateSteamgiftsGiveawayModal/index.js:
--------------------------------------------------------------------------------
1 | import CreateSteamgiftsGiveawayModal from './CreateSteamgiftsGiveawayModal';
2 |
3 | export default CreateSteamgiftsGiveawayModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/GameInfoModal/index.js:
--------------------------------------------------------------------------------
1 | import GameInfoModal from './GameInfoModal';
2 |
3 | export default GameInfoModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/ImportModal/index.js:
--------------------------------------------------------------------------------
1 | import ImportModal from "./ImportModal";
2 |
3 | export default ImportModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/NewModal/index.js:
--------------------------------------------------------------------------------
1 | import NewModal from "./NewModal";
2 |
3 | export default NewModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/SearchModal/SearchModal.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Modal, Search, Segment, Header, Item, Icon, Container, Button, Input, Divider } from "semantic-ui-react";
3 | import _ from 'lodash';
4 |
5 | import ItadApi from "../../../../lib/itad/ItadApi";
6 |
7 | function SearchModal({ onSelect, initialValue, children }) {
8 | const [showModal, setShowModal] = React.useState(false)
9 | const [isSearching, setIsSearching] = React.useState(false)
10 | const [searchResults, setSearchResults] = React.useState(null)
11 | const [value, setValue] = React.useState(initialValue)
12 | const manualInputRef = React.createRef();
13 |
14 | React.useEffect(() => { }, [])
15 |
16 | const Child = React.Children.only(children);
17 | const newChildren = React.cloneElement(Child, { onClick: openModal });
18 |
19 | function openModal() { setShowModal(true) }
20 | function closeModal() { setShowModal(false) }
21 |
22 | function handleResultSelect(e, { result }) {
23 | onSelect(result);
24 | closeModal();
25 | }
26 |
27 | function handleSearchChange(e, { value }) {
28 | if (e.type === "focus" && searchResults !== null) { return }
29 |
30 | setValue(value)
31 | if (isSearching || !value || value.length < 3) return
32 | setIsSearching(true)
33 | ItadApi.FindGame(value).then(response => {
34 | console.log("response", response);
35 |
36 | const uniqueResultsObject = response.data.data.list.reduce((acc, item) => Object.assign(acc, { [item.plain]: item }), {});
37 | const uniqueResultsArray = Object.keys(uniqueResultsObject).map(s => ({ ...uniqueResultsObject[s] }));
38 | const filteredResults = uniqueResultsArray.reduce((results, item) => {
39 | const categoryName = item.urls.buy.split('/')[3].toUpperCase();
40 | const appId = item.urls.buy.split('/')[4];
41 | const newResult = {
42 | 'title': item.title,
43 | 'appid': appId,
44 | 'image': categoryName === "APP" ? `https://steamcdn-a.akamaihd.net/steam/apps/${appId}/header.jpg` : "",
45 | 'plain': item.plain,
46 | 'urls': {
47 | 'steam': item.urls.buy,
48 | 'itad': item.urls.game
49 | }
50 | }
51 |
52 | return results[categoryName]
53 | ? {
54 | ...results,
55 | [categoryName]: {
56 | "name": categoryName,
57 | "results": results[categoryName].results.concat(newResult)
58 | }
59 | }
60 | : {
61 | ...results,
62 | [categoryName]: {
63 | "name": categoryName,
64 | "results": [newResult]
65 | }
66 | }
67 | }, {})
68 |
69 | console.log("filteredResults", filteredResults);
70 |
71 | setIsSearching(false);
72 | setSearchResults(filteredResults);
73 | }, response => { console.error(response); setIsSearching(false); })
74 | }
75 |
76 | const categoryLayoutRenderer = ({ categoryContent, resultsContent }) => (
77 |
78 | {categoryContent}
79 |
80 | {resultsContent}
81 |
82 |
83 | )
84 |
85 | const categoryRenderer = ({ name }) => (
86 |
89 | )
90 |
91 | const resultRenderer = ({ title, image }) => (
92 |
93 | -
94 | {image !== "" &&
}
95 |
96 |
97 |
98 | )
99 |
100 | function handleManualSelect(e) {
101 | onSelect(manualInputRef.current.value);
102 | closeModal();
103 | }
104 |
105 | return (
106 | }
108 | trigger={newChildren}
109 | centered={false}
110 | size="small"
111 | open={showModal}
112 | >
113 | Find Game
114 |
115 |
116 |
131 | Or
132 |
133 |
134 | Change
135 |
136 |
137 |
138 |
139 | );
140 | }
141 |
142 | export default SearchModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/SearchModal/index.js:
--------------------------------------------------------------------------------
1 | import SearchModal from "./SearchModal";
2 |
3 | export default SearchModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/SetColumnSettingsModal/SetColumnSettingsModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState, } from "react";
2 | import { Modal, Button, Confirm, Container, Segment, Form, } from "semantic-ui-react";
3 | import { useSelector, useDispatch } from "react-redux";
4 |
5 | import { setNewRowChange } from "../../../../store/actions/TableActions";
6 | import FieldSettings from "../../FieldSettings";
7 |
8 | import useFormValidation from '../../../../hooks/useFormValidation'
9 | import validateHeaderSetting from '../../../../hooks/formValidations/validateHeaderSetting'
10 | import useInterval from '../../../../hooks/useInterval'
11 |
12 | function SetColumnSettingsModal({ triggerElement, headerLabel, }) {
13 | const dispatch = useDispatch()
14 | const headers = useSelector((state) => state.table.changes.headers)
15 |
16 | const handleOpen = () => setModalOpen(true)
17 | const handleClose = () => setModalOpen(false)
18 |
19 | const Child = React.Children.only(triggerElement);
20 | const newChildren = React.cloneElement(Child, { onClick: handleOpen });
21 |
22 | const [modalOpen, setModalOpen] = useState(false)
23 |
24 | const INITIAL_STATE = headers[headerLabel];
25 |
26 | const { handleSubmit, handleChange, values, errors } = useFormValidation(INITIAL_STATE, validateHeaderSetting, saveHeaderSettings);
27 |
28 | const [isFinishedAlertTimerRunning, setIsFinishedAlertTimerRunning] = useState(false);
29 | const [handleSubmitEvent, setHandleSubmitEvent] = useState(null);
30 |
31 | useInterval(() => {
32 | setIsFinishedAlertTimerRunning(false)
33 | handleSubmit(handleSubmitEvent)
34 | }, isFinishedAlertTimerRunning ? 1 : null);
35 |
36 | function onSubmit(event) {
37 | if (values.options !== headers[headerLabel].options) {
38 | setHandleSubmitEvent(event)
39 | setIsFinishedAlertTimerRunning(true)
40 | }
41 |
42 | handleSubmit(event)
43 | }
44 |
45 | function saveHeaderSettings() {
46 | const newValues = {
47 | ...headers[headerLabel],
48 | ...values
49 | }
50 | dispatch(setNewRowChange('headers', {
51 | ...headers,
52 | [headerLabel]: newValues
53 | }))
54 | handleClose()
55 | }
56 |
57 | const modalContent = (
58 |
59 |
72 |
73 | )
74 |
75 | return (
76 | Save}
84 | trigger={newChildren}
85 | />
86 | )
87 | }
88 |
89 | export default SetColumnSettingsModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/SetColumnSettingsModal/index.js:
--------------------------------------------------------------------------------
1 | import SetColumnSettingsModal from "./SetColumnSettingsModal";
2 |
3 | export default SetColumnSettingsModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/ShareModal/ShareModal.js:
--------------------------------------------------------------------------------
1 | import React, { useState, } from "react";
2 | import { Modal, Button, Confirm, Container, Message, Segment, Label, List, Grid, Icon, } from "semantic-ui-react";
3 | import { useSelector, useDispatch } from "react-redux";
4 |
5 | import { showShareModal } from "../../../../store/actions/TableActions";
6 | import { getPrivateColumns } from "../../../../utils";
7 | import DataFilters from "../../DataFilters";
8 | import Spreadsheets from "../../../../lib/google/Spreadsheets";
9 |
10 | function ShareModal({ triggerElement }) {
11 | const dispatch = useDispatch()
12 | const showModal = useSelector((state) => state.table.showShareModal)
13 | const spreadsheetId = useSelector((state) => state.authentication.currentSpreadsheetId)
14 | const steamProfile = useSelector((state) => state.authentication.steam.profile)
15 | const headers = useSelector((state) => state.table.headers)
16 | const filters = useSelector((state) => state.filters)
17 |
18 | const [isExportingSpreadsheet, setIsExportingSpreadsheet] = useState(false);
19 | const [exportedSheetUrl, setExportedSheetUrl] = useState(null);
20 |
21 | const handleCloseModal = () => dispatch(showShareModal(false))
22 |
23 | function exportSpreadsheet() {
24 | setIsExportingSpreadsheet(true)
25 |
26 | Spreadsheets.ExportSpreadsheet(spreadsheetId, getPrivateColumns(headers), filters, steamProfile.personaname, headers)
27 | .then(response => {
28 | if (response.success) {
29 | console.log(response.data)
30 | setExportedSheetUrl(response.data)
31 | } else { }
32 | })
33 | .finally(response => {
34 | setIsExportingSpreadsheet(false)
35 | })
36 | }
37 |
38 | const shareVideoTutorialModal = (
39 | }
49 | closeIcon={true}
50 | >
51 |
52 |
53 |
54 |
55 |
56 |
57 | Your browser does not support the video tag.
58 |
59 |
60 |
61 |
62 |
63 | )
64 |
65 | const modalContent = (
66 |
67 |
68 |
69 |
70 | Exporting will create a new spreadsheet according to the filters you've applied to your spreadsheet,
71 | After exporting you will get a link to your new spreadsheet so you can share with whomever you want
72 |
73 |
74 |
75 | Applied Filters
76 |
77 |
78 |
79 | Private Fields
80 |
81 | {
82 | Object.keys(headers)
83 | .filter(headerKey => headers[headerKey].isPrivate === true)
84 | .map((headerKey, index) => (
85 |
86 |
87 | {headers[headerKey].label}
88 |
89 |
90 | ))
91 | }
92 |
93 |
94 | Info
95 |
96 | You need to change the Spreadsheet's permission before sharing the link {shareVideoTutorialModal}
97 | Private Fields: You can set private columns in Settings and they will be removed from the new spreadsheet
98 |
99 |
100 |
101 |
102 |
103 |
104 | {
105 | exportedSheetUrl && (
106 |
107 | Exported
108 | {
109 |
117 | }
118 |
119 | )
120 | }
121 |
122 |
123 | )
124 |
125 | return (
126 | Export}
134 | trigger={triggerElement}
135 | />
136 | )
137 | }
138 |
139 | export default ShareModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/ShareModal/index.js:
--------------------------------------------------------------------------------
1 | import ShareModal from "./ShareModal";
2 |
3 | export default ShareModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Modals/TableSettingsModal/index.js:
--------------------------------------------------------------------------------
1 | import TableSettingsModal from "./TableSettingsModal";
2 |
3 | export default TableSettingsModal;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/OptionsEditor/index.js:
--------------------------------------------------------------------------------
1 | import OptionsEditor from "./OptionsEditor";
2 |
3 | export default OptionsEditor;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Settings/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { Button, Form, Grid, Header, Message, Segment, } from 'semantic-ui-react'
4 |
5 | import ErrorBox from '../../ErrorBox';
6 | import { setupComplete, spreadsheetSetId, steamSetApiKey, steamSetProfile, steamLogged } from '../../../store/actions/AuthenticationActions';
7 | import ImportModal from '../Modals/ImportModal';
8 |
9 | import useFormValidation from '../../../hooks/useFormValidation'
10 | import validateSettings from '../../../hooks/formValidations/validateSettings'
11 | import usePrevious from '../../../hooks/usePrevious'
12 | import useInterval from '../../../hooks/useInterval'
13 | import useLocalStorage from '../../../hooks/useLocalStorage';
14 |
15 | import SteamApi from '../../../lib/steam/SteamApi';
16 | import Spreadsheets from '../../../lib/google/Spreadsheets';
17 |
18 | function Settings() {
19 | const steam = useSelector((state) => state.authentication.steam)
20 | const spreadsheetId = useSelector((state) => state.authentication.spreadsheetId)
21 |
22 | const dispatch = useDispatch()
23 |
24 | const prevSteamProfile = usePrevious(steam.profile);
25 |
26 | const [isSaveSuccess, setIsSaveSuccess] = useState(false);
27 | const [isImportSuccess, setIsImportSuccess] = useState(false);
28 | const [isFinishedAlertTimerRunning, setIsFinishedAlertTimerRunning] = useState(false);
29 | const [creatingSpreadsheet, setCreatingSpreadsheet] = useState(false);
30 |
31 | const [steamStorage,] = useLocalStorage("steam", null)
32 | const [spreadsheetIdStorage,] = useLocalStorage("spreadsheetId", null)
33 |
34 | const INITIAL_STATE = steam.loggedIn || (JSON.parse(steamStorage) && JSON.parse(steamStorage).loggedIn)
35 | ? {
36 | spreadsheetId: spreadsheetIdStorage || '',
37 | steamApiKey: (JSON.parse(steamStorage) && JSON.parse(steamStorage).apiKey) || '',
38 | }
39 | : {
40 | spreadsheetId: spreadsheetIdStorage || '',
41 | }
42 |
43 | const { handleSubmit, handleChange, values, errors, } = useFormValidation(INITIAL_STATE, validateSettings, handleUpdate);
44 |
45 | useInterval(() => {
46 | setIsSaveSuccess(false)
47 | setIsImportSuccess(false)
48 | setIsFinishedAlertTimerRunning(false)
49 | }, isFinishedAlertTimerRunning ? 5000 : null);
50 |
51 | useEffect(() => {
52 | if (steam.loggedIn !== null && spreadsheetId) {
53 | dispatch(setupComplete(true))
54 | return
55 | }
56 |
57 | if (steam.id && steam.apiKey) {
58 | if (!steam.profile) {
59 | SteamApi.GetUserInfo(steam.id, steam.apiKey)
60 | .then(response => {
61 | if (response.success) {
62 | dispatch(steamSetProfile(response.data.user))
63 | }
64 | })
65 | }
66 |
67 | if (prevSteamProfile === null && steam.profile) {
68 | dispatch(steamLogged(true))
69 | }
70 | }
71 | }, [steam, spreadsheetId])
72 |
73 | function handleUpdate() {
74 | dispatch(steamSetApiKey(values.steamApiKey))
75 | dispatch(spreadsheetSetId(values.spreadsheetId))
76 |
77 | setIsSaveSuccess(true)
78 | setIsFinishedAlertTimerRunning(true)
79 | }
80 |
81 | function createSpreadsheet(event) {
82 | event.preventDefault();
83 | setCreatingSpreadsheet(true)
84 | Spreadsheets.CreateStartingSpreadsheet("My Collection")
85 | .then(response => {
86 | if (response.success) {
87 | handleChange(event, { name: "spreadsheetId", value: response.data.spreadsheetId })
88 | }
89 | })
90 | .finally(response => {
91 | setCreatingSpreadsheet(false)
92 | })
93 | }
94 |
95 | function handleImport(event, response) {
96 | setIsImportSuccess(true)
97 | setIsFinishedAlertTimerRunning(true)
98 | handleChange(event, { name: 'spreadsheetId', value: response.spreadsheetId })
99 | }
100 |
101 | return (
102 |
103 |
104 |
105 |
106 | {
107 | !isImportSuccess
108 | ?
109 | Already have a spreadsheet?
110 |
111 | Click Here} /> to import
112 |
113 |
114 | :
115 | Success!
116 |
117 | Import successful!
118 |
119 |
120 | }
121 |
122 |
165 |
166 | {
167 | isSaveSuccess && (
168 |
169 | Saved!
170 |
171 | )
172 | }
173 | {
174 | steam.loggedIn !== false && (
175 |
176 | Info
177 |
178 | Get your Steam Web API Key Here
179 |
180 |
181 | )
182 | }
183 |
184 |
185 | )
186 | }
187 |
188 | export default Settings
--------------------------------------------------------------------------------
/src/components/KeysDbApp/Settings/index.js:
--------------------------------------------------------------------------------
1 | import Settings from "./Settings";
2 |
3 | export default Settings;
--------------------------------------------------------------------------------
/src/components/KeysDbApp/SortDropdown/SortDropdown.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Header, Dropdown, Grid, Button, Icon } from "semantic-ui-react";
3 | import { useSelector, useDispatch } from "react-redux";
4 |
5 | import { changeOrderby } from "../../../store/actions/TableActions";
6 |
7 | function SortDropdown() {
8 | const headers = useSelector((state) => state.table.headers)
9 | const orderBy = useSelector((state) => state.table.orderBy)
10 |
11 | const dispatch = useDispatch()
12 |
13 | return (
14 |
15 |
16 |
20 |
21 | {
22 | Object.keys(headers)
23 | .filter(header => header !== "ID" && headers[header].sortable)
24 | .map((key, index) => (
25 |
30 |
31 |
32 | {key}
33 |
34 |
35 |
36 | { dispatch(changeOrderby({ sort: key, asc: true })) }}
40 | >
41 |
50 |
51 |
52 | { dispatch(changeOrderby({ sort: key, asc: false })) }}
56 | >
57 |
66 |
67 |
68 |
69 |
70 |
71 | ))
72 | }
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
80 | export default SortDropdown;
81 |
--------------------------------------------------------------------------------
/src/components/KeysDbApp/SortDropdown/index.js:
--------------------------------------------------------------------------------
1 | import SortDropdown from './SortDropdown';
2 |
3 | export default SortDropdown;
--------------------------------------------------------------------------------
/src/components/auth/GoogleLoginComponent/GoogleLoginComponent.js:
--------------------------------------------------------------------------------
1 | import React, { } from "react";
2 | import { Button, Grid } from "semantic-ui-react";
3 |
4 | export function GoogleLoginComponent({ isAuthenticated, handleSignIn, handleSignOut, }) {
5 | return (
6 | <>
7 | {
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {isAuthenticated ? 'Sign out' : 'Sign in with Google'}
27 |
28 |
29 |
30 | }
31 | >
32 | )
33 | }
--------------------------------------------------------------------------------
/src/components/auth/GoogleLoginComponent/index.js:
--------------------------------------------------------------------------------
1 | import GoogleLoginComponent from './GoogleLoginComponent';
2 |
3 | export default GoogleLoginComponent;
--------------------------------------------------------------------------------
/src/components/auth/Login.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from 'react-router-dom';
3 | import firebase from '../../firebase';
4 | import { Container, Button } from "semantic-ui-react";
5 |
6 | function Login({ }) {
7 |
8 | function onLogin() {
9 | var provider = new firebase.auth.GoogleAuthProvider();
10 | provider.addScope('https://www.googleapis.com/auth/spreadsheets');
11 | provider.setCustomParameters({
12 | 'login_hint': 'user@example.com'
13 | });
14 | firebase.auth().signInWithPopup(provider).then(function (result) {
15 | // This gives you a Google Access Token. You can use it to access the Google API.
16 | var token = result.credential.accessToken;
17 | // The signed-in user info.
18 | var user = result.user;
19 | // ...
20 | }).catch(function (error) {
21 | debugger
22 | // Handle Errors here.
23 | var errorCode = error.code;
24 | var errorMessage = error.message;
25 | // The email of the user's account used.
26 | var email = error.email;
27 | // The firebase.auth.AuthCredential type that was used.
28 | var credential = error.credential;
29 | // ...
30 | });
31 | }
32 |
33 | return (
34 |
35 | Login
36 |
37 | );
38 | }
39 |
40 | export default Login;
41 |
--------------------------------------------------------------------------------
/src/components/auth/SteamLogin/SteamLogin.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, } from "react-redux";
3 | import { Container, Message, List, Header, Button, } from "semantic-ui-react";
4 |
5 | import { steamLogged } from "../../../store/actions/AuthenticationActions";
6 | import SteamLoginComponent from "../SteamLoginComponent/SteamLoginComponent";
7 | import useSteam from "../../../hooks/useSteam";
8 |
9 | function SteamLogin() {
10 | const dispatch = useDispatch()
11 |
12 | const { handleSignIn, isAuthenticated, } = useSteam({
13 | env: window.location.origin,
14 | returnTo: 'get-started',
15 | })
16 |
17 | function skip() {
18 | dispatch(steamLogged(false))
19 | }
20 |
21 | return (
22 | isAuthenticated === false && (
23 |
24 |
25 |
26 |
27 | Steam is optional but it is highly recommended
28 |
29 |
30 | Steam is used for:
31 |
32 | Checking if you own a game you're adding
33 | Checking if games are on your wishlist
34 | And more...
35 |
36 |
37 |
38 | Skip
39 |
40 |
41 |
42 | )
43 | )
44 | }
45 |
46 | export default SteamLogin;
--------------------------------------------------------------------------------
/src/components/auth/SteamLogin/index.js:
--------------------------------------------------------------------------------
1 | import SteamLogin from './SteamLogin';
2 |
3 | export default SteamLogin;
--------------------------------------------------------------------------------
/src/components/auth/SteamLoginComponent/SteamLoginComponent.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Image } from "semantic-ui-react";
3 |
4 | function SteamLoginComponent({ handleSignIn }) {
5 | return (
6 |
12 | )
13 |
14 | // return (
15 | //
16 | //
26 | //
27 | // )
28 | }
29 |
30 | export default SteamLoginComponent;
--------------------------------------------------------------------------------
/src/components/auth/SteamLoginComponent/index.js:
--------------------------------------------------------------------------------
1 | import SteamLoginComponent from './SteamLoginComponent';
2 |
3 | export default SteamLoginComponent;
--------------------------------------------------------------------------------
/src/constants/spreadsheetConstants.js:
--------------------------------------------------------------------------------
1 | export const SPREADSHEET_METADATA_HEADERS_ID = 1986
2 | export const SPREADSHEET_METADATA_PERMISSIONS_ID = 1988
3 | export const SPREADSHEET_METADATA_SHEET_ID = 1910
4 | export const SPREADSHEET_METADATA_DEFAULT_SETTINGS = { "ID": { "id": "A", "label": "ID", "type": "number", "pattern": "General", "display": false }, "Title": { "id": "B", "label": "Title", "type": "steam_title", "isPrivate": false, "display": true, "isFilter": false, "sortable": false }, "Status": { "id": "C", "label": "Status", "type": "dropdown", "isPrivate": false, "options": { "allowEdit": false, "values": [{ "value": "Used", "color": "red" }, { "value": "Unused", "color": "green" }, { "value": "Traded", "color": "yellow" }, { "value": "Gifted", "color": "orange" }] }, "display": true, "isFilter": true, "sortable": true }, "Key": { "id": "D", "label": "Key", "type": "key", "isPrivate": true, "display": true, "isFilter": false, "sortable": false }, "From": { "id": "E", "label": "From", "type": "dropdown", "isPrivate": false, "options": { "allowEdit": true, "values": [{ "value": "Fanatical", "color": "green" }, { "value": "Indiegala", "color": "red" }, { "value": "Other", "color": "grey" }, { "value": "Amazon", "color": "brown" }, { "value": "Alienware", "color": "blue" }, { "value": "AMD", "color": "orange" }, { "value": "Indiegamestand", "color": "pink" }, { "value": "Sega", "color": "blue" }, { "value": "DigitalHomicide", "color": "brown" }, { "value": "Humblebundle", "color": "blue" }] }, "display": true, "isFilter": true, "sortable": true }, "Own Status": { "id": "F", "label": "Own Status", "type": "steam_ownership", "isPrivate": false, "options": { "allowEdit": false, "values": [{ "value": "Own", "color": "green" }, { "value": "Missing", "color": "red" }] }, "display": true, "isFilter": true, "sortable": true }, "Date Added": { "id": "G", "label": "Date Added", "type": "date", "pattern": "dd-mm-yyyy", "isPrivate": true, "display": true, "isFilter": true, "sortable": true }, "Note": { "id": "H", "label": "Note", "type": "text", "isPrivate": true, "display": true, "isFilter": false, "sortable": false }, "isthereanydeal URL": { "id": "I", "label": "isthereanydeal URL", "type": "url", "isPrivate": false, "display": true, "isFilter": false, "sortable": false }, "Steam URL": { "id": "J", "label": "Steam URL", "type": "steam_url", "isPrivate": false, "display": true, "isFilter": false, "sortable": false }, "Cards": { "id": "K", "label": "Cards", "type": "steam_cards", "isPrivate": false, "options": { "allowEdit": false, "values": [{ "value": "Have", "color": "green" }, { "value": "Missing", "color": "red" }] }, "display": true, "isFilter": true, "sortable": true }, "AppId": { "id": "L", "label": "AppId", "type": "steam_appid", "pattern": "General", "isPrivate": false, "display": true, "isFilter": false, "sortable": false } }
5 | export const SPREADSHEET_TEMPLATE_SPREADSHEET_ID = '13WFCn_RDuz9ZaCS4fj5VkpCUTz8HuIhSTYRjSXC-7bU'
6 | export const SPREADSHEET_IMPORT_TEMPLATE_SPREADSHEET_ID = '1qlzwzis9pyxI_C2s534oOPDjCaMp8ou_nTQ_SClZTxg'
--------------------------------------------------------------------------------
/src/constants/statisticsConstants.js:
--------------------------------------------------------------------------------
1 | export const COLOR_PALLETES = [
2 | //https://learnui.design/tools/data-color-picker.html#divergent
3 | //https://color.adobe.com/explore
4 | [
5 | "#4b5359",
6 | "#545f6e",
7 | "#656883",
8 | "#7e7093",
9 | "#9d769d",
10 | "#be7b9f",
11 | "#dd8299",
12 | "#f58c8d",
13 | ],
14 | [
15 | "#0788d9",
16 | "#009ee2",
17 | "#00b0d8",
18 | "#00bfbd",
19 | "#00cb98",
20 | "#5cd46e",
21 | "#acd749",
22 | "#f2d338",
23 | ],
24 | [
25 | "#ec2125",
26 | "#f15105",
27 | "#f07400",
28 | "#eb9400",
29 | "#e1b100",
30 | "#d4cc00",
31 | "#c2e61f",
32 | "#aaff53",
33 | ],
34 | [
35 | "#8c116b",
36 | "#8258a8",
37 | "#6f8ad1",
38 | "#73b5e5",
39 | "#9cdcf0",
40 | "#d7ffff",
41 | "#befdfd",
42 | "#a4fafa",
43 | "#85f8f8",
44 | "#5ef5f5",
45 | "#05f2f2",
46 | ],
47 | [
48 | "#009418",
49 | "#00ab51",
50 | "#00c180",
51 | "#00d6ad",
52 | "#00ebd8",
53 | "#40ffff",
54 | "#0ccadc",
55 | "#0097b2",
56 | "#036784",
57 | "#083c54",
58 | "#011526",
59 | ],
60 | // [
61 | // "#d9303e",
62 | // "#e26743",
63 | // "#e99256",
64 | // "#eeb876",
65 | // "#f4db9f",
66 | // "#fffbcf",
67 | // "#ecf2ad",
68 | // "#d3ea8e",
69 | // "#b4e371",
70 | // "#8ddc58",
71 | // "#58d543",
72 | // ],
73 | [
74 | "#020540",
75 | "#3b2a62",
76 | "#6b5187",
77 | "#9c7cad",
78 | "#cdaad5",
79 | "#ffdbff",
80 | "#ffb7e8",
81 | "#ff90c3",
82 | "#ff6591",
83 | "#ff3855",
84 | "#f20505",
85 | ],
86 | [
87 | "#0ed2e9",
88 | "#26dbdb",
89 | "#50e2c8",
90 | "#77e8b4",
91 | "#9eeba0",
92 | "#c4ed8f",
93 | "#cbce60",
94 | "#d2ae37",
95 | "#d8891a",
96 | "#db6016",
97 | "#d92525",
98 | ],
99 | [
100 | "#b246f2",
101 | "#6789ff",
102 | "#0cb4ff",
103 | "#50d3ff",
104 | "#a1ecff",
105 | "#ecffff",
106 | "#bcfef6",
107 | "#8dfbe1",
108 | "#66f6c1",
109 | "#4eef97",
110 | "#4ee662",
111 | ],
112 | [
113 | "#52188c",
114 | "#902888",
115 | "#bb4986",
116 | "#d9708a",
117 | "#ef9997",
118 | "#ffc2b0",
119 | "#ffbb93",
120 | "#f5b875",
121 | "#e2b957",
122 | "#c6bb3b",
123 | "#9ebf26",
124 | ],
125 | [
126 | "#d92525",
127 | "#ef4e62",
128 | "#fa7798",
129 | "#fd9fc5",
130 | "#fcc6e8",
131 | "#ffeaff",
132 | "#ece1fc",
133 | "#d6d9f8",
134 | "#bdd1f1",
135 | "#a4cae7",
136 | "#8bc3d9",
137 | ],
138 | ]
139 | export const PIE_CHART_CHUNK_MOBILE = 1;
140 | export const PIE_CHART_CHUNK_DESKTOP = 2;
141 | export const PIE_CHART_CHUNK_LARGER_DESKTOP = 3;
142 | export const LINE_CHART_CHUNK = 1;
--------------------------------------------------------------------------------
/src/constants/steamConstants.js:
--------------------------------------------------------------------------------
1 | export const STEAM_CATEGORIES = {
2 | 1: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
3 | 2: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_singlePlayer.png',
4 | 6: 'https://steamstore-a.akamaihd.net/public/images/ico/ico_mod_hl2.gif',
5 | 7: 'https://steamstore-a.akamaihd.net/public/images/ico/ico_mod_hl.gif',
6 | 8: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_vac.png',
7 | 9: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_coop.png',
8 | 13: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_cc.png',
9 | 14: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_commentary.png',
10 | 15: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_stats.png',
11 | 16: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_sdk.png',
12 | 17: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_editor.png',
13 | 18: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_partial_controller.png',
14 | 19: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_sdk.png',
15 | 20: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
16 | 21: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_dlc.png',
17 | 22: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_achievements.png',
18 | 23: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_cloud.png',
19 | 24: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_coop.png',
20 | 25: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_leaderboards.png',
21 | 27: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
22 | 28: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_controller.png',
23 | 29: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_cards.png',
24 | 30: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_workshop.png',
25 | 32: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_turn_notifications.png',
26 | 35: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_cart.png',
27 | 36: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
28 | 37: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
29 | 38: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_coop.png',
30 | 39: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_coop.png',
31 | 40: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_collectibles.png',
32 | 41: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_remote_play.png',
33 | 42: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_remote_play.png',
34 | 43: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_remote_play.png',
35 | 44: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_remote_play_together.png',
36 | 47: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_multiPlayer.png',
37 | 48: 'https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_coop.png',
38 | }
--------------------------------------------------------------------------------
/src/constants/tableConstants.js:
--------------------------------------------------------------------------------
1 | export const TABLE_DEFAULT_OFFSET = 0
2 | export const TABLE_DEFAULT_LIMIT = 24
3 | export const TABLE_DEFAULT_ACTIVEPAGE = 1
--------------------------------------------------------------------------------
/src/firebase/context.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const FirebaseContext = React.createContext(null);
4 |
5 | export default FirebaseContext;
--------------------------------------------------------------------------------
/src/firebase/firebase.js:
--------------------------------------------------------------------------------
1 | import app from 'firebase/app';
2 | import 'firebase/auth';
3 | import 'firebase/analytics'
4 | import 'firebase/firestore';
5 |
6 | import firebaseConfig from './config';
7 |
8 | // Initialize Firebase
9 |
10 | class Firebase {
11 | constructor() {
12 | app.initializeApp(firebaseConfig);
13 |
14 | const isLocalhost = Boolean(
15 | window.location.hostname === 'localhost' ||
16 | // [::1] is the IPv6 localhost address.
17 | window.location.hostname === '[::1]' ||
18 | // 127.0.0.1/8 is considered localhost for IPv4.
19 | window.location.hostname.match(
20 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
21 | )
22 | );
23 |
24 | if (!isLocalhost) {
25 | app.analytics();
26 | }
27 | }
28 | }
29 |
30 | const firebase = new Firebase();
31 |
32 | export default firebase;
--------------------------------------------------------------------------------
/src/firebase/index.js:
--------------------------------------------------------------------------------
1 | import firebase from './firebase';
2 | import FirebaseContext from './context';
3 |
4 | export { FirebaseContext };
5 | export default firebase;
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateHeaderSetting.js:
--------------------------------------------------------------------------------
1 | import { isDropdownType } from "../../utils";
2 |
3 | export default function validateHeaderSetting(values) {
4 | let errors = {};
5 |
6 | if (!values.label) {
7 | errors.label = "Label cannot be empty"
8 | }
9 |
10 | // Type Errors
11 | if (!values.type) {
12 | errors.type = "Type must be selected";
13 | }
14 |
15 | if (isDropdownType(values.type) && (!values.options || (values.options && values.options.values.length === 0))) {
16 | errors.type = "Needs at least one option";
17 | }
18 |
19 | return errors;
20 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateImport.js:
--------------------------------------------------------------------------------
1 | export default function validateImport(values) {
2 | let errors = {};
3 |
4 | // Spreadsheet Errors
5 | if (!values.spreadsheetId) {
6 | errors.spreadsheetId = "Spreadsheet ID is required";
7 | }
8 |
9 | return errors;
10 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateNewKey.js:
--------------------------------------------------------------------------------
1 | export default function validateNewKey(values) {
2 | let errors = {};
3 |
4 | // Title Errors
5 | if (!values['Title']) {
6 | errors['Title'] = "Title is required";
7 | } else if (values['Title'].length === 0) {
8 | errors.title = "Title must have at least 1 character";
9 | }
10 |
11 | // Date Errors
12 | if (!values["Date Added"]) {
13 | errors["Date Added"] = "Date is required";
14 | }
15 |
16 | return errors;
17 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateOption.js:
--------------------------------------------------------------------------------
1 | export default function validateOption(values) {
2 | let errors = {};
3 |
4 | // Value Errors
5 | if (values.value === "") {
6 | errors.value = "Option must be filled";
7 | }
8 |
9 | return errors;
10 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateSettings.js:
--------------------------------------------------------------------------------
1 | export default function validateSettings(values) {
2 | let errors = {};
3 |
4 | // Spreadsheet Errors
5 | if (!values.spreadsheetId) {
6 | errors.spreadsheetId = "Spreadsheet ID is required";
7 | }
8 |
9 | // Steam Web Api Key Errors
10 | if (values.steamApiKey !== undefined && !values.steamApiKey) {
11 | errors.steamApiKey = "Steam Web Api is required"
12 | }
13 |
14 | return errors;
15 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateSteamgiftsGiveaway.js:
--------------------------------------------------------------------------------
1 | export default function validateSteamgiftsGiveaway(values) {
2 | let errors = {};
3 |
4 | // AppId Errors
5 | if (!values['appid']) {
6 | errors['appid'] = "App ID is required";
7 | }
8 |
9 | // Title Errors
10 | if (!values['title']) {
11 | errors['title'] = "Title is required";
12 | }
13 |
14 | // Key Errors
15 | if (!values["key"]) {
16 | errors["key"] = "Steam Key is required.";
17 | }
18 |
19 | // Starting Offset Errors
20 | if (!values["startingTimeOffset"]) {
21 | errors["startingTimeOffset"] = "Starting offset is required.";
22 | }
23 |
24 | // Giveaway Time Errors
25 | if (!values["timeActive"]) {
26 | errors["timeActive"] = "Giveaway time is required.";
27 | } else if (values["timeActive"] < 60) {
28 | errors["timeActive"] = "Must be at least 60 minutes.";
29 | }
30 |
31 | return errors;
32 | }
--------------------------------------------------------------------------------
/src/hooks/formValidations/validateTableSettings.js:
--------------------------------------------------------------------------------
1 | import { isDropdownType } from "../../utils";
2 |
3 | export default function validateTableSettings(values) {
4 | let errors = {};
5 |
6 | const types = {
7 | steam_title: { selected: false, text: '(Steam) Title' },
8 | steam_url: { selected: false, text: '(Steam) URL' },
9 | steam_appid: { selected: false, text: '(Steam) App Id' },
10 | steam_key: { selected: false, text: '(Steam) Key' },
11 | steam_cards: { selected: false, text: '(Steam) Cards' },
12 | steam_achievements: { selected: false, text: '(Steam) Achievements' },
13 | steam_dlc: { selected: false, text: '(Steam) DLC' },
14 | steam_bundled: { selected: false, text: '(Steam) Bundled' },
15 | steam_ownership: { selected: false, text: '(Steam) Owned' },
16 | }
17 |
18 | Object.keys(values).forEach((key, index) => {
19 | const targetedValues = values[key]
20 |
21 | // Type errors
22 | if (!targetedValues.type) {
23 | errors[key] = {
24 | ...errors[key],
25 | type: `You must select a type.`
26 | }
27 | } else if (types[targetedValues.type] && types[targetedValues.type].selected === false) {
28 | types[targetedValues.type].selected = true
29 | } else if (types[targetedValues.type] && types[targetedValues.type].selected === true) {
30 | errors[key] = {
31 | ...errors[key],
32 | type: `${types[targetedValues.type].text} type can only be selected once`
33 | }
34 | }
35 |
36 | // Dropdown errors
37 | if (isDropdownType(targetedValues.type)) {
38 | if (!targetedValues.options || !targetedValues.options.values || targetedValues.options.values.length === 0) {
39 | errors[key] = {
40 | ...errors[key],
41 | options: `Missing Options`
42 | }
43 | }
44 | }
45 | })
46 |
47 | return errors;
48 | }
--------------------------------------------------------------------------------
/src/hooks/useBottomPage.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | // Custom hook that will alert once reaching the bottom of the page
4 | function useBottomPage(offset = 100) {
5 | const [bottom, setBottom] = useState(false);
6 |
7 | useEffect(() => {
8 | function handleScroll() {
9 | // const isBottom = Math.ceil(window.innerHeight + document.documentElement.scrollTop) === document.documentElement.scrollHeight;
10 | const isBottom = document.documentElement.scrollTop !== 0 && window.innerHeight + document.documentElement.scrollTop > document.documentElement.scrollHeight - offset
11 | setBottom(isBottom);
12 | }
13 | window.addEventListener("scroll", handleScroll);
14 | return () => {
15 | window.removeEventListener("scroll", handleScroll);
16 | };
17 | }, []);
18 |
19 | return bottom;
20 | }
21 |
22 | export default useBottomPage;
--------------------------------------------------------------------------------
/src/hooks/useFormValidation.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | function useFormValidation(initialState, validate, authenticate) {
4 | const [values, setValues] = useState(initialState);
5 | const [errors, setErrors] = useState({});
6 | const [isSubmitting, setSubmitting] = useState(false);
7 |
8 | useEffect(() => {
9 | if (isSubmitting) {
10 | const noErrors = Object.keys(errors).length === 0;
11 |
12 | if (noErrors) {
13 | authenticate();
14 | setSubmitting(false);
15 | } else {
16 | setSubmitting(false);
17 | }
18 | }
19 | }, [errors])
20 |
21 | function reset() {
22 | setValues(initialState)
23 | }
24 |
25 | function updateValues(event, values) {
26 | if (event && event.persist) {
27 | event.persist();
28 | }
29 |
30 | setValues(previousValues => ({
31 | ...previousValues,
32 | ...Object.keys(values).reduce((result, item) => ({
33 | ...result,
34 | [values[item].header]: values[item].value !== undefined ? values[item].value : values[item].checked
35 | }), {})
36 | }))
37 | }
38 |
39 | function handleChange(event, data, key = null) {
40 | if (event && event.persist) {
41 | event.persist();
42 | }
43 |
44 | if (key) {
45 | setValues(previousValues => ({
46 | ...previousValues,
47 | [key]: {
48 | ...previousValues[key],
49 | [data.name]: data.value || data.checked,
50 | // values: {
51 | // ...previousValues[key].values,
52 | // [data.name]: data.value || data.checked,
53 | // }
54 | }
55 | }));
56 | } else {
57 | setValues(previousValues => ({
58 | ...previousValues,
59 | [data.name]: data.value === undefined
60 | ? data.checked
61 | : data.value
62 | }));
63 | }
64 | }
65 |
66 | function handleBlur() {
67 | const validationErrors = validate(values);
68 | setErrors(validationErrors);
69 | }
70 |
71 | function handleSubmit(event) {
72 | event.preventDefault();
73 | const validationErrors = validate(values);
74 | setErrors(validationErrors);
75 | setSubmitting(true);
76 | }
77 |
78 | function handleSetNewValue(newValue) {
79 | setValues(newValue)
80 | }
81 |
82 | return { handleSubmit, handleBlur, handleChange, updateValues, reset, values, errors, isSubmitting, handleSetNewValue }
83 | }
84 |
85 | export default useFormValidation;
--------------------------------------------------------------------------------
/src/hooks/useGapi.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, } from 'react';
2 | import { useDispatch, useSelector } from 'react-redux';
3 | import { useHistory } from 'react-router-dom';
4 | import { gapi } from 'gapi-script';
5 |
6 | import { googleLoggedIn, googleLoggedOut, googleClientReady } from '../store/actions/AuthenticationActions';
7 | import useLocalStorage from './useLocalStorage';
8 |
9 | // Custom hook to initialize and use the Google API
10 | function useGapi(options,) {
11 | const google = useSelector((state) => state.authentication.google);
12 |
13 | const [isAuthenticated, setIsAuthenticated] = useState(false);
14 | const [currentUser, setCurrentUser] = useState(null);
15 | const [isLoading, setIsLoading] = useState(true);
16 |
17 | const dispatch = useDispatch();
18 | const history = useHistory();
19 |
20 | const [, setGoogleTokenStorage] = useLocalStorage("gTokenId", null)
21 |
22 | const {
23 | apiKey,
24 | clientId,
25 | discoveryDocs,
26 | scope,
27 | ux_mode,
28 | // redirect_uri,
29 | onLoaded,
30 | } = options;
31 |
32 | useEffect(() => {
33 | gapi.load("client:auth2", initClient);
34 | }, []);
35 |
36 | function initClient() {
37 | // Initialize the JavaScript client library.
38 | gapi.client
39 | .init({
40 | apiKey,
41 | discoveryDocs,
42 | clientId,
43 | scope,
44 | ux_mode,
45 | // redirect_uri
46 | })
47 | .then(() => {
48 | // Listen for sign-in state changes.
49 | gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
50 |
51 | // Handle the initial sign-in state.
52 | updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
53 |
54 | // Initialize and make the API request.
55 | !google.googleClientReady && dispatch(googleClientReady(true))
56 | onLoaded && onLoaded()
57 | setIsLoading(false)
58 | });
59 | };
60 |
61 | function updateSigninStatus(isSignedIn) {
62 | if (!isSignedIn) {
63 | // gapi.auth2.getAuthInstance().signIn();
64 | } else {
65 | setGoogleTokenStorage(gapi.client.getToken().access_token)
66 |
67 | const userInfo = gapi.auth2.getAuthInstance().currentUser.get().getBasicProfile();
68 | const basicProfile = {
69 | id: userInfo && userInfo.getId(),
70 | fullName: userInfo && userInfo.getName(),
71 | givenName: userInfo && userInfo.getGivenName(),
72 | familyName: userInfo && userInfo.getFamilyName(),
73 | imageUrl: userInfo && userInfo.getImageUrl(),
74 | email: userInfo && userInfo.getEmail(),
75 | };
76 |
77 | setIsAuthenticated(true);
78 | setCurrentUser(basicProfile);
79 | google.profile === null && dispatch(googleLoggedIn(basicProfile))
80 | }
81 | };
82 |
83 | async function handleSignIn() {
84 | try {
85 | await gapi.auth2.getAuthInstance().signIn();
86 | } catch (error) {
87 | console.log(error);
88 | throw new Error('Google API not loaded', error);
89 | }
90 | };
91 |
92 | async function handleSignOut() {
93 | try {
94 | await gapi.auth2.getAuthInstance().signOut();
95 | setGoogleTokenStorage(null)
96 | setIsAuthenticated(false);
97 | dispatch(googleLoggedOut());
98 | history.push('/');
99 | } catch (error) {
100 | console.log(error);
101 | throw new Error('Google API not loaded', error);
102 | }
103 | };
104 |
105 | return {
106 | isLoading,
107 | currentUser,
108 | isAuthenticated,
109 | handleSignIn,
110 | handleSignOut
111 | };
112 | }
113 |
114 | export default useGapi;
--------------------------------------------------------------------------------
/src/hooks/useInterval.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, } from 'react';
2 |
3 | // Custom hook for an setTimeout
4 | function useInterval(callback, delay) {
5 | const savedCallback = useRef();
6 |
7 | // Remember the latest callback.
8 | useEffect(() => {
9 | savedCallback.current = callback;
10 | }, [callback]);
11 |
12 | // Set up the interval.
13 | useEffect(() => {
14 | function tick() {
15 | savedCallback.current();
16 | }
17 | if (delay !== null) {
18 | let id = setInterval(tick, delay);
19 | return () => clearInterval(id);
20 | }
21 | }, [delay]);
22 | }
23 |
24 | export default useInterval;
--------------------------------------------------------------------------------
/src/hooks/useLocalStorage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const getCache = (key, initial) => {
4 | const cached = localStorage.getItem(key)
5 |
6 | if (cached === null && initial !== null) {
7 | localStorage.setItem(key, initial)
8 | }
9 |
10 | return cached !== null ? cached : initial
11 | }
12 |
13 | // Usage: const [state, setState] = useLocalStorage("LOCAL_STORAGE_KEY", initialValue)
14 | const useLocalStorage = (key, initial) => {
15 | const [nativeState, setNativeState] = React.useState(getCache(key, initial))
16 | const setState = state => {
17 | if (typeof state === 'function') {
18 | setNativeState(prev => {
19 | const newState = state(prev)
20 | localStorage.setItem(key, newState)
21 | return newState
22 | })
23 | } else {
24 | localStorage.setItem(key, state)
25 | setNativeState(state)
26 | }
27 | }
28 |
29 | return [nativeState, setState]
30 | }
31 | export default useLocalStorage
--------------------------------------------------------------------------------
/src/hooks/usePrevious.js:
--------------------------------------------------------------------------------
1 | import { useRef, useEffect } from "react";
2 |
3 | // Custom hook for saving previous value
4 | function usePrevious(value) {
5 | const ref = useRef();
6 |
7 | useEffect(() => {
8 | ref.current = value;
9 | });
10 | return ref.current;
11 | }
12 |
13 | export default usePrevious;
--------------------------------------------------------------------------------
/src/hooks/useRecharts.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CartesianGrid, Cell, Legend, Line, LineChart, Pie, PieChart, Tooltip, XAxis, YAxis, } from 'recharts';
3 |
4 | function useRecharts() {
5 | const RADIAN = Math.PI / 180;
6 |
7 | const renderCustomizedLabel = ({ cx, cy, midAngle, outerRadius, percent, name, value, fill }) => {
8 | const radius = outerRadius + 25;
9 | const x = cx + radius * Math.cos(-midAngle * RADIAN);
10 | const y = cy + radius * Math.sin(-midAngle * RADIAN);
11 |
12 | return (
13 | cx ? 'start' : 'end'} dominantBaseline="central" >
14 | {`${name}: ${value} (${(percent * 100).toFixed(0)}%)`}
15 |
16 | );
17 | };
18 |
19 | function renderPieChart(options) {
20 | const { data, isDonut, colors, width } = options
21 |
22 | return (
23 |
24 |
35 | {
36 | data.map((entry, index) => | )
37 | }
38 |
39 |
40 | {/* */}
41 |
42 | )
43 | }
44 |
45 | function renderLineChart(options) {
46 | const { data, dataKey, width } = options
47 |
48 | return (
49 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | return {
66 | renderPieChart,
67 | renderLineChart,
68 | };
69 | }
70 |
71 | export default useRecharts;
--------------------------------------------------------------------------------
/src/hooks/useUrlParams.js:
--------------------------------------------------------------------------------
1 | // import { useLocation } from "react-router-dom";
2 |
3 | import { useState } from "react";
4 | import { useEffect } from "react";
5 |
6 | // Custom hook for manipulating url params
7 | function useUrlParams(location) {
8 | const [urlParamsObject, setUrlParamsObject] = useState(null)
9 |
10 | useEffect(() => {
11 | if (!urlParamsObject) {
12 | setUrlParamsObject(toParamObject())
13 | }
14 | }, [])
15 |
16 | function toParamObject(queryString = location.search) {
17 | if (urlParamsObject) {
18 | return urlParamsObject
19 | }
20 |
21 | const params = new URLSearchParams(queryString);
22 | let paramObject = {};
23 |
24 | params.forEach((value, key) => {
25 | if (paramObject[key]) {
26 | paramObject = {
27 | ...paramObject,
28 | [key]: [
29 | ...paramObject[key],
30 | value,
31 | ],
32 | };
33 | } else {
34 | paramObject = {
35 | ...paramObject,
36 | [key]: [value],
37 | };
38 | }
39 | });
40 |
41 | return paramObject
42 | }
43 |
44 | const toQueryString = (paramObject) => {
45 | let queryString = '';
46 |
47 | Object.entries(paramObject).forEach(([paramKey, paramValue]) => {
48 | if (paramValue.length === 0) {
49 | return;
50 | }
51 | queryString += '?';
52 | paramValue.forEach((value, index) => {
53 | if (index > 0) {
54 | queryString += '&';
55 | }
56 | queryString += `${paramKey}=${value}`;
57 | });
58 | });
59 |
60 | // This is kind of hacky, but if we push '' as the route, we lose
61 | // our page, and base path etc.
62 | // So instead.. pushing a '?' just removes all the current query strings
63 | return queryString !== '' ? queryString : '?';
64 | };
65 |
66 | const cleanUrl = () => {
67 | window.history.replaceState({}, document.title, `${location.pathname}`);
68 | }
69 |
70 | return {
71 | urlParamsObject,
72 | toParamObject,
73 | toQueryString,
74 | cleanUrl,
75 | }
76 | }
77 |
78 | export default useUrlParams;
--------------------------------------------------------------------------------
/src/hooks/useWindowDimensions.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | function getWindowDimensions() {
4 | const { innerWidth: width, innerHeight: height } = window;
5 | return {
6 | width,
7 | height
8 | };
9 | }
10 |
11 | export default function useWindowDimensions() {
12 | const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
13 |
14 | useEffect(() => {
15 | function handleResize() {
16 | setWindowDimensions(getWindowDimensions());
17 | }
18 |
19 | window.addEventListener('resize', handleResize);
20 | return () => window.removeEventListener('resize', handleResize);
21 | }, []);
22 |
23 | return windowDimensions;
24 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from 'react-redux';
4 | import * as serviceWorker from "./serviceWorker";
5 |
6 | import store from "./store/store";
7 |
8 | import 'semantic-ui-css/semantic.min.css';
9 | import './styles/index.css'
10 |
11 | import App from "./components/App"
12 |
13 | ReactDOM.render(
14 |
15 |
16 |
17 | , document.getElementById("root"));
18 |
19 | // If you want your app to work offline and load faster, you can change
20 | // unregister() to register() below. Note this comes with some pitfalls.
21 | // Learn more about service workers: https://bit.ly/CRA-PWA
22 | serviceWorker.unregister();
23 |
--------------------------------------------------------------------------------
/src/lib/itad/ItadApi.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import _ from 'lodash';
3 | import itadConfig from './config';
4 |
5 | const apiKey = itadConfig.apiKey;
6 |
7 | function _romanize(num) {
8 | var key = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
9 | return key[Number(num)];
10 | }
11 |
12 | function _encodeName(str) {
13 | if (str === undefined || str === null) { return "" }
14 |
15 | str = str.toLowerCase(); //lowercase
16 | str = str.replace(/[1-9]/g, _romanize);//_romanize digits
17 | str = str.replace(/(^the[^a-z])|([^a-z]the[^a-z])|([^a-z]the$)/g, ""); //remove "the", but not e.g. "other" or "them"
18 | str = str.replace(/([^a-z]the[^a-z])|([^a-z]the$)/g, ""); //remove "the", but not e.g. "other" or "them"
19 | str = str.replace(/\+/g, "plus"); //spell out "plus"
20 | str = str.replace(/&/g, "and"); //spell out "and"
21 | str = str.replace(/&/g, "and"); //spell out "and"
22 | str = str.replace(/[^a-z0]/g, ''); //remove remaining invalid characters, like spaces, braces, hyphens etc
23 | return str;
24 | }
25 |
26 | function GetInfoAboutBundles(title) {
27 | return axios.get(`https://api.isthereanydeal.com/v01/game/bundles/?key=${apiKey}&plains=${_encodeName(title)}`)
28 | .then(response => (
29 | {
30 | success: response.status === 200 ? true : false,
31 | times_bundled: response.status === 200
32 | ? response.data.data[_encodeName(title)].total
33 | : null,
34 | bundle_url: response.status === 200
35 | ? response.data.data[_encodeName(title)].urls.bundles
36 | : null,
37 | }))
38 | .catch(reason => (
39 | {
40 | success: false,
41 | times_bundled: null,
42 | bundle_url: null,
43 | }))
44 | }
45 |
46 | function GetOverview(name, appid, type) {
47 | const plainName = _encodeName(name);
48 |
49 | return axios.get(`https://api.isthereanydeal.com/v01/game/overview/?key=${apiKey}&allowed=steam&plains=${plainName}${appid ? `&ids=${type}/${appid}` : ''}`)
50 | .then(response => {
51 | if (response.status === 200) {
52 | return {
53 | success: true,
54 | data: response.data.data[plainName]
55 | }
56 | } else {
57 | return {
58 | success: false,
59 | data: ""
60 | }
61 | }
62 | })
63 | .catch(response => {
64 | return {
65 | success: false,
66 | data: response
67 | }
68 | })
69 | }
70 |
71 | function GetInfoAboutGame(gameName) {
72 | const plainName = _encodeName(gameName);
73 | return axios.get(`https://api.isthereanydeal.com/v01/game/info/?key=${apiKey}&plains=${plainName}`)
74 | .then(response => {
75 | return {
76 | success: response.status === 200 ? true : false,
77 | data: response.data.data[plainName]
78 | }
79 | })
80 | }
81 |
82 | function GetPlain(gameName) { return axios.get(`https://api.isthereanydeal.com/v02/game/plain/?key=${apiKey}&title=${gameName}`); }
83 |
84 | function FindGame(query) { return axios.get(`https://api.isthereanydeal.com/v01/search/search/?key=${apiKey}&q=${query}&shops=steam`); }
85 |
86 | function GetPlainName(map, appid) {
87 | return map.data[`app/${appid}`] || map.data[`sub/${appid}`] || map.data[`bundle/${appid}`]
88 | }
89 |
90 | function GetMap(shop = 'steam') {
91 | return axios.get(`https://api.isthereanydeal.com/v01/game/map/?key=${apiKey}&shop=${shop}&type=id:plain`)
92 | .then(response => {
93 | if (response.status === 200 && !_.isEmpty(response.data.data)) {
94 | return {
95 | success: true,
96 | data: {
97 | itadMap: {
98 | data: response.data.data,
99 | timestamp: new Date()
100 | }
101 | }
102 | }
103 | }
104 |
105 | return response
106 | })
107 | }
108 |
109 | export default {
110 | GetInfoAboutBundles,
111 | GetOverview,
112 | GetInfoAboutGame,
113 | GetPlain,
114 | FindGame,
115 | GetPlainName,
116 | GetMap,
117 | }
--------------------------------------------------------------------------------
/src/lib/steam/SteamApi.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import _ from 'lodash'
3 | import { corsLink } from '../../utils'
4 |
5 | // https://wiki.teamfortress.com/wiki/User:RJackson/StorefrontAPI#Known_methods
6 |
7 | function _get(url, params = {}) {
8 | return axios.get(url, {
9 | headers: {
10 | 'Access-Control-Allow-Origin': '*',
11 | 'X-Requested-With': 'XMLHttpRequest',
12 | // 'Origin': 'https://keys-db.web.app/',
13 | },
14 | ...params
15 | })
16 | .then(response => {
17 | if (response.status === 200) {
18 | return {
19 | success: true,
20 | data: response.data
21 | }
22 | } else {
23 | return {
24 | success: false,
25 | error: response
26 | }
27 | }
28 | })
29 | .catch(reason => ({
30 | success: false,
31 | error: reason
32 | }));
33 | }
34 |
35 | function GetPackageDetails(packageid) {
36 | return _get(`${corsLink('https://store.steampowered.com/api/packagedetails/')}`, {
37 | params: {
38 | packageids: packageid,
39 | }
40 | })
41 | }
42 |
43 | function GetAppDetails(appid) {
44 | return _get(`${corsLink('https://store.steampowered.com/api/appdetails/')}`, {
45 | params: {
46 | appids: appid,
47 | }
48 | })
49 | }
50 |
51 | function GetOwnedGames(steamId, steamApiKey) {
52 | // return fetch(`https://api.allorigins.win/get?url=${encodeURIComponent('https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/')}`,
53 | // {
54 | // params: {
55 | // steamids: steamId,
56 | // key: steamApiKey,
57 | // format: 'json'
58 | // }
59 | // })
60 | // .then(response => {
61 | // if (response.ok) return response.json()
62 | // throw new Error('Network response was not ok.')
63 | // })
64 | // .then(data => console.log(data.contents));
65 |
66 | return _get(corsLink(`http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/`), {
67 | params: {
68 | steamid: steamId,
69 | key: steamApiKey,
70 | format: 'json'
71 | }
72 | })
73 | .then(response => {
74 | if (response.success === true) {
75 | const data = {
76 | count: response.data.response.game_count,
77 | games: response.data.response.games.reduce((acc, game) => (_.concat(acc, [game.appid])), [])
78 | }
79 |
80 | return {
81 | success: true,
82 | data: {
83 | games: {
84 | ...data,
85 | timestamp: new Date()
86 | }
87 | }
88 | }
89 | }
90 |
91 | return response
92 | })
93 | }
94 |
95 | function GetUserInfo(steamId, steamApiKey) {
96 | return _get(corsLink(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/`), {
97 | params: {
98 | steamids: steamId,
99 | key: steamApiKey,
100 | format: 'json'
101 | }
102 | })
103 | .then(response => {
104 | if (response.success === true) {
105 | return {
106 | success: true,
107 | data: {
108 | user: response.data.response.players[0]
109 | }
110 | }
111 | } else {
112 | return response
113 | }
114 | })
115 | .catch(reason => ({ success: false, error: reason }));
116 | }
117 |
118 | function DoesUserOwnGame(ownedGames, appid) {
119 | let result = false;
120 |
121 | try {
122 | result = ownedGames.find(app => app === parseInt(appid)) != null
123 | } catch (error) {
124 |
125 | }
126 |
127 | return result
128 | }
129 |
130 | export default {
131 | GetAppDetails,
132 | GetOwnedGames,
133 | GetUserInfo,
134 | DoesUserOwnGame,
135 | GetPackageDetails,
136 | }
--------------------------------------------------------------------------------
/src/pages/ErrorPage/ErrorPage.js:
--------------------------------------------------------------------------------
1 | import React, { } from "react"
2 | import { Segment, Header, Icon, Button, Grid, Message } from "semantic-ui-react";
3 | import { useHistory } from "react-router-dom";
4 |
5 | function ErrorPage(props) {
6 | const errorCode = props.match.params.error
7 | const history = useHistory()
8 |
9 | function showError(errorCode) {
10 | switch (errorCode) {
11 | case 'missing_settings':
12 | return (
13 |
14 |
15 |
16 |
17 | Missing Settings
18 |
19 | Looks like your spreadsheet is missing crucial settings,
20 | Try importing this spreadsheet and map the headers.
21 |
22 |
23 |
24 |
25 |
26 | )
27 | case 'unauthorized':
28 | return (
29 |
30 |
31 |
32 |
33 | Unauthorized
34 |
35 | Looks like you're not authorized to view this,
36 | Make sure the owner had shared correctly.
37 |
38 |
39 |
40 |
41 |
42 | )
43 | default:
44 | return (
45 |
46 |
47 |
48 |
49 | Error
50 |
51 | Something broke... sorry about this...
52 | Please open an issue here (Github) and I'll try getting on it...
53 |
54 |
55 |
56 |
57 |
58 | )
59 | }
60 | }
61 |
62 | return (
63 |
64 |
65 |
66 | Ooops...
67 |
68 | {showError(errorCode)}
69 |
70 |
71 | history.push(`/`)}>Home
72 |
73 | )
74 | }
75 |
76 | export default ErrorPage;
--------------------------------------------------------------------------------
/src/pages/KeysDBPage/KeysDBPage.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useDispatch, useSelector, } from "react-redux";
3 | import { useHistory, Redirect } from "react-router-dom";
4 | import { Dimmer, Loader } from "semantic-ui-react";
5 | import dateFns from 'date-fns'
6 |
7 | import KeysTable from "../../components/KeysDbApp/KeysTable/KeysTable";
8 | import { addHeaders, } from "../../store/actions/TableActions";
9 | import { itadSetMap, spreadsheetSetPermission, setCurrentSpreadsheetId, steamSetOwnedGames, setCurrentSheetId, } from "../../store/actions/AuthenticationActions";
10 |
11 | import usePrevious from '../../hooks/usePrevious'
12 |
13 | import SteamApi from "../../lib/steam/SteamApi";
14 | import ItadApi from "../../lib/itad/ItadApi";
15 | import Spreadsheets from "../../lib/google/Spreadsheets";
16 | import useLocalStorage from "../../hooks/useLocalStorage";
17 |
18 | function KeysDBPage(props) {
19 | const spreadsheetId = props.match.params.spreadsheetId || useSelector((state) => state.authentication.spreadsheetId)
20 | const google = useSelector((state) => state.authentication.google)
21 | const steam = useSelector((state) => state.authentication.steam)
22 | const itad = useSelector((state) => state.authentication.itad)
23 |
24 | const [initSpreadsheet, setInitSpreadsheet] = useState(true)
25 | const [spreadsheetReady, setSpreadsheetReady] = useState(false)
26 | const [error, setError] = useState({ hasError: false })
27 | const prevSpreadsheetId = usePrevious(spreadsheetId)
28 |
29 | const [loadingOwnedGames, setLoadingOwnedGames] = useState(false)
30 | const [loadingItadMap, setLoadingItadMap] = useState(false)
31 |
32 | const [itadStorage,] = useLocalStorage("itad", null)
33 |
34 | const dispatch = useDispatch()
35 | const history = useHistory()
36 |
37 | useEffect(() => {
38 | if (google.googleClientReady && (google.loggedIn === null || google.loggedIn === false)) {
39 | history.push(`/get-started`)
40 | }
41 |
42 | if (prevSpreadsheetId && spreadsheetId) {
43 | if (prevSpreadsheetId !== spreadsheetId) {
44 | setSpreadsheetReady(false)
45 | setInitSpreadsheet(true)
46 | }
47 | }
48 |
49 | if (error.hasError) {
50 | return
51 | }
52 |
53 | if (steam.loggedIn === true && !loadingOwnedGames && (steam.ownedGames === null || dateFns.differenceInDays(new Date(), steam.ownedGames.timestamp) > 1)) {
54 | setLoadingOwnedGames(true)
55 |
56 | SteamApi.GetOwnedGames(steam.id, steam.apiKey)
57 | .then(response => {
58 | if (!response.success) {
59 | console.error(response.data)
60 | return
61 | }
62 |
63 | dispatch(steamSetOwnedGames(response.data.games))
64 | setLoadingOwnedGames(false)
65 | })
66 | }
67 |
68 | if (!loadingItadMap && itad.map === null) {
69 | const itadJson = JSON.parse(itadStorage)
70 |
71 | if (itadJson && itadJson.map && dateFns.differenceInWeeks(new Date(), itadJson.map.timestamp) === 0) {
72 | dispatch(itadSetMap(itadJson.map))
73 | } else {
74 | setLoadingItadMap(true)
75 |
76 | ItadApi.GetMap()
77 | .then(response => {
78 | if (!response.success) {
79 | console.error(response.data)
80 | return
81 | }
82 |
83 | dispatch(itadSetMap(response.data.itadMap))
84 | setLoadingItadMap(false)
85 | })
86 | }
87 | }
88 |
89 | if (google.googleClientReady && initSpreadsheet) {
90 | Spreadsheets.Initialize(spreadsheetId)
91 | .then(response => {
92 | if (response.success) {
93 | dispatch(spreadsheetSetPermission(response.permissions))
94 | dispatch(addHeaders(response.headers))
95 | dispatch(setCurrentSheetId(response.sheetId))
96 | dispatch(setCurrentSpreadsheetId(spreadsheetId))
97 |
98 | setSpreadsheetReady(true)
99 | setInitSpreadsheet(false)
100 | } else {
101 | if (response.error === "PERMISSION_DENIED") {
102 | dispatch(spreadsheetSetPermission("unauthorized"))
103 | setError({ hasError: true, code: "unauthorized" })
104 | } else if (response.error === "MISSING_SETTINGS") {
105 | dispatch(spreadsheetSetPermission("missing_settings"))
106 | setError({ hasError: true, code: "missing_settings" })
107 | }
108 | else {
109 | dispatch(spreadsheetSetPermission("RESOURCE_EXHAUSTED"))
110 | setError({ hasError: true, code: "RESOURCE_EXHAUSTED" })
111 | }
112 | }
113 | })
114 | }
115 | }, [google, steam, spreadsheetId, initSpreadsheet, error])
116 |
117 | return (
118 | error.hasError
119 | ? ( )
120 | : !google.googleClientReady || !spreadsheetId || !spreadsheetReady
121 | ? (
122 |
123 |
124 |
125 | )
126 | : (
127 |
128 | )
129 | )
130 | }
131 |
132 | export default KeysDBPage;
--------------------------------------------------------------------------------
/src/pages/SetupPage/SetupPage.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, } from "react"
2 | import { useHistory } from 'react-router-dom'
3 | import { useSelector, } from "react-redux"
4 | import { Container, Step, Grid, } from "semantic-ui-react"
5 |
6 | import Settings from "../../components/KeysDbApp/Settings"
7 | import SteamLogin from "../../components/auth/SteamLogin"
8 | import useGapi from '../../hooks/useGapi'
9 | import googleConfig from "../../lib/google/config"
10 | import { GoogleLoginComponent } from "../../components/auth/GoogleLoginComponent/GoogleLoginComponent"
11 |
12 | function SetupPage() {
13 | const isSteamLogged = useSelector((state) => state.authentication.steam.loggedIn)
14 | const setupComplete = useSelector((state) => state.authentication.setupComplete)
15 | const spreadsheetId = useSelector((state) => state.authentication.spreadsheetId)
16 | const steam = useSelector((state) => state.authentication.steam)
17 |
18 | const history = useHistory()
19 | const googleApi = useGapi(googleConfig);
20 | const { isAuthenticated, handleSignIn, isLoading, } = googleApi;
21 |
22 | useEffect(() => {
23 | if (setupComplete) {
24 | history.push(`/id/${spreadsheetId}`)
25 | }
26 | }, [setupComplete,])
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Google
37 | Login with google
38 |
39 |
40 |
41 |
42 | Steam
43 | Login with steam
44 |
45 |
46 |
47 |
48 | Set Up
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {
58 | !isAuthenticated && !isLoading && (
59 |
60 |
64 |
65 | )
66 | }
67 | {
68 | isAuthenticated && (!steam.id && isSteamLogged === null) &&
69 | }
70 | {
71 | isAuthenticated && (steam.id || isSteamLogged !== null) && !setupComplete &&
72 | }
73 | {/* {
74 | isGoogleLogged && isSteamLogged && setupComplete && (
75 |
76 |
77 |
78 | )
79 | } */}
80 |
81 |
82 |
83 | )
84 | }
85 |
86 | export default SetupPage;
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/store/actionTypes/AuthenticationActionTypes.js:
--------------------------------------------------------------------------------
1 | // Authentication Action Types
2 |
3 | export const GOOGLE_LOGGED_IN = 'GOOGLE_LOGGED_IN';
4 | export const GOOGLE_LOGGED_OUT = 'GOOGLE_LOGGED_OUT';
5 | export const GOOGLE_CLIENT_READY = 'GOOGLE_CLIENT_READY';
6 |
7 | export const STEAM_SET_ID = 'STEAM_SET_ID';
8 | export const STEAM_SET_API_KEY = 'STEAM_SET_API_KEY';
9 | export const STEAM_SET_PROFILE = 'STEAM_SET_PROFILE';
10 | export const STEAM_LOGGED = 'STEAM_LOGGED';
11 | export const STEAM_LOAD = 'STEAM_LOAD';
12 | export const STEAM_SET_OWNED_GAMES = 'STEAM_SET_OWNED_GAMES';
13 |
14 | export const ITAD_SET_MAP = 'ITAD_SET_MAP';
15 |
16 | export const SPREADSHEET_SET_ID = 'SPREADSHEET_SET_ID';
17 | export const SET_CURRENT_SPREADSHEET_ID = 'SET_CURRENT_SPREADSHEET_ID';
18 | export const SET_CURRENT_SHEET_ID = 'SET_CURRENT_SHEET_ID';
19 | export const SET_UP_COMPLETE = 'SET_UP_COMPLETE';
20 | export const SET_SPREADSHEET_PERMISSION = 'SET_SPREADSHEET_PERMISSION';
--------------------------------------------------------------------------------
/src/store/actionTypes/FilterActionTypes.js:
--------------------------------------------------------------------------------
1 | // Filters Action Types
2 |
3 | export const ADD_FILTER = 'ADD_FILTER';
4 | export const REMOVE_FILTER = 'REMOVE_FILTER';
--------------------------------------------------------------------------------
/src/store/actionTypes/ImportActionTypes.js:
--------------------------------------------------------------------------------
1 | // Import Action Types
2 |
3 | export const SET_IMPORTED_HEADERS = 'SET_IMPORTED_HEADERS';
4 | export const SET_IMPORTED_HEADER = 'SET_IMPORTED_HEADER';
--------------------------------------------------------------------------------
/src/store/actionTypes/StatisticsActionsTypes.js:
--------------------------------------------------------------------------------
1 | // Statistics Actions Types
2 |
3 | export const LOAD_STATISTICS_SPREADSHEET = 'LOAD_STATISTICS_SPREADSHEET';
4 | export const CLEAR_STATISTICS_SPREADSHEET = 'CLEAR_STATISTICS_SPREADSHEET';
5 |
6 | export const LOAD_STATISTICS_CHARTS = 'LOAD_STATISTICS_CHARTS';
7 | export const CLEAR_STATISTICS_CHARTS = 'CLEAR_STATISTICS_CHARTS';
8 |
9 | export const RESET_STATISTICS_STORAGE = 'RESET_STATISTICS_STORAGE';
10 |
11 |
--------------------------------------------------------------------------------
/src/store/actionTypes/TableActionTypes.js:
--------------------------------------------------------------------------------
1 | // Table Action Types
2 |
3 | export const ADD_HEADERS = 'ADD_HEADERS';
4 | export const REMOVE_HEADERS = 'REMOVE_HEADERS';
5 |
6 | export const SET_IS_TABLE_EMPTY = 'SET_IS_TABLE_EMPTY';
7 | export const RESET_TABLE_PARAMS = 'RESET_TABLE_PARAMS';
8 | export const RELOAD_TABLE = 'RELOAD_TABLE';
9 | export const CHANGE_ORDER_BY = 'CHANGE_ORDER_BY';
10 | export const CHANGE_PAGE_SIZE = 'CHANGE_PAGE_SIZE';
11 |
12 | export const SET_CURRENT_ROWS = 'SET_CURRENT_ROWS';
13 | export const UPDATE_ROW = 'UPDATE_ROW';
14 |
15 | export const SET_NEW_ROW_CHANGE = 'SET_NEW_ROW';
16 | export const REMOVE_NEW_ROW_CHANGE = 'REMOVE_NEW_ROW';
17 |
18 | export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
--------------------------------------------------------------------------------
/src/store/actionTypes/ThemeActionTypes.js:
--------------------------------------------------------------------------------
1 | // Theme Action Types
2 |
3 | export const CHANGE_THEME = 'CHANGE_THEME';
--------------------------------------------------------------------------------
/src/store/actions/AuthenticationActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/AuthenticationActionTypes';
2 |
3 | // Authentication Action
4 | export const steamSetApiKey = apiKey => {
5 | return {
6 | type: actionTypes.STEAM_SET_API_KEY,
7 | payload: apiKey
8 | }
9 | }
10 | export const steamSetId = key => {
11 | return {
12 | type: actionTypes.STEAM_SET_ID,
13 | payload: key
14 | }
15 | }
16 | export const steamSetProfile = profile => {
17 | return {
18 | type: actionTypes.STEAM_SET_PROFILE,
19 | payload: profile
20 | }
21 | }
22 | export const steamLogged = state => {
23 | return {
24 | type: actionTypes.STEAM_LOGGED,
25 | payload: state
26 | }
27 | }
28 | export const steamLoad = steam => {
29 | return {
30 | type: actionTypes.STEAM_LOAD,
31 | payload: steam
32 | }
33 | }
34 | export const steamSetOwnedGames = games => {
35 | return {
36 | type: actionTypes.STEAM_SET_OWNED_GAMES,
37 | payload: games
38 | }
39 | }
40 | export const itadSetMap = data => {
41 | return {
42 | type: actionTypes.ITAD_SET_MAP,
43 | payload: data
44 | }
45 | }
46 |
47 | export const setupComplete = state => {
48 | return {
49 | type: actionTypes.SET_UP_COMPLETE,
50 | payload: state
51 | }
52 | }
53 | export const spreadsheetSetId = id => {
54 | return {
55 | type: actionTypes.SPREADSHEET_SET_ID,
56 | payload: id
57 | }
58 | }
59 | export const setCurrentSpreadsheetId = id => {
60 | return {
61 | type: actionTypes.SET_CURRENT_SPREADSHEET_ID,
62 | payload: id
63 | }
64 | }
65 | export const setCurrentSheetId = id => {
66 | return {
67 | type: actionTypes.SET_CURRENT_SHEET_ID,
68 | payload: id
69 | }
70 | }
71 | export const spreadsheetSetPermission = permission => {
72 | return {
73 | type: actionTypes.SET_SPREADSHEET_PERMISSION,
74 | payload: permission
75 | }
76 | }
77 |
78 | export const googleLoggedOut = () => {
79 | return {
80 | type: actionTypes.GOOGLE_LOGGED_OUT,
81 | }
82 | }
83 | export const googleLoggedIn = profile => {
84 | return {
85 | type: actionTypes.GOOGLE_LOGGED_IN,
86 | payload: profile
87 | }
88 | }
89 | export const googleClientReady = state => {
90 | return {
91 | type: actionTypes.GOOGLE_CLIENT_READY,
92 | payload: state
93 | }
94 | }
--------------------------------------------------------------------------------
/src/store/actions/FilterActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/FilterActionTypes';
2 |
3 | // Filters Actions
4 | export const addFilter = filter => {
5 | return {
6 | type: actionTypes.ADD_FILTER,
7 | payload: {
8 | key: filter.key,
9 | values: filter.values,
10 | id: filter.id
11 | }
12 | }
13 | }
14 |
15 | export const removeFilter = filter => {
16 | return {
17 | type: actionTypes.REMOVE_FILTER,
18 | payload: {
19 | key: filter.key,
20 | value: filter.value,
21 | id: filter.id
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/store/actions/ImportActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/ImportActionTypes';
2 |
3 | // Import Actions
4 | export const setImportedHeaders = headers => {
5 | return {
6 | type: actionTypes.SET_IMPORTED_HEADERS,
7 | payload: headers
8 | }
9 | }
10 |
11 | export const setImportedHeader = header => {
12 | return {
13 | type: actionTypes.SET_IMPORTED_HEADER,
14 | payload: header
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/store/actions/StatisticsActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/StatisticsActionsTypes';
2 |
3 | // Statistics Actions
4 |
5 | export const loadStatisticsSpreadsheet = spreadsheetData => {
6 | return {
7 | type: actionTypes.LOAD_STATISTICS_SPREADSHEET,
8 | payload: spreadsheetData
9 | }
10 | }
11 |
12 | export const clearStatisticsSpreadsheet = () => {
13 | return {
14 | type: actionTypes.CLEAR_STATISTICS_SPREADSHEET
15 | }
16 | }
17 |
18 | export const loadStatisticsCharts = charts => {
19 | return {
20 | type: actionTypes.LOAD_STATISTICS_CHARTS,
21 | payload: charts
22 | }
23 | }
24 |
25 | export const clearStatisticsCharts = () => {
26 | return {
27 | type: actionTypes.CLEAR_STATISTICS_CHARTS
28 | }
29 | }
30 |
31 | export const resetStatisticsStorage = () => {
32 | return {
33 | type: actionTypes.RESET_STATISTICS_STORAGE
34 | }
35 | }
--------------------------------------------------------------------------------
/src/store/actions/TableActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/TableActionTypes';
2 |
3 | // Table Actions
4 |
5 | export const setCurrentRows = rows => {
6 | return {
7 | type: actionTypes.SET_CURRENT_ROWS,
8 | payload: rows
9 | }
10 | }
11 |
12 | export const updateRow = (index, row) => {
13 | return {
14 | type: actionTypes.UPDATE_ROW,
15 | payload: {
16 | index: index,
17 | row: row
18 | }
19 | }
20 | }
21 |
22 | export const changePageSize = size => {
23 | return {
24 | type: actionTypes.CHANGE_PAGE_SIZE,
25 | payload: size
26 | }
27 | }
28 |
29 | export const changeOrderby = orderBy => {
30 | return {
31 | type: actionTypes.CHANGE_ORDER_BY,
32 | payload: orderBy
33 | }
34 | }
35 |
36 | export const resetTableParams = paramsToReset => {
37 | return {
38 | type: actionTypes.RESET_TABLE_PARAMS,
39 | payload: paramsToReset
40 | }
41 | }
42 |
43 | export const reloadTable = state => {
44 | return {
45 | type: actionTypes.RELOAD_TABLE,
46 | payload: state
47 | }
48 | }
49 |
50 | export const addHeaders = headers => {
51 | return {
52 | type: actionTypes.ADD_HEADERS,
53 | payload: headers
54 | }
55 | }
56 |
57 | export const removeHeaders = () => {
58 | return {
59 | type: actionTypes.REMOVE_HEADERS
60 | }
61 | }
62 |
63 | export const setNewRowChange = (id, row) => {
64 | return {
65 | type: actionTypes.SET_NEW_ROW_CHANGE,
66 | payload: {
67 | id: id,
68 | row: row
69 | }
70 | }
71 | }
72 |
73 | export const removeNewRowChange = (id) => {
74 | return {
75 | type: actionTypes.REMOVE_NEW_ROW_CHANGE,
76 | payload: id
77 | }
78 | }
79 |
80 | export const setIsTableEmpty = state => {
81 | return {
82 | type: actionTypes.SET_IS_TABLE_EMPTY,
83 | payload: state
84 | }
85 | }
86 |
87 | export const showShareModal = state => {
88 | return {
89 | type: actionTypes.SHOW_SHARE_MODAL,
90 | payload: state
91 | }
92 | }
--------------------------------------------------------------------------------
/src/store/actions/ThemeActions.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../actionTypes/ThemeActionTypes';
2 |
3 | // Theme Actions
4 | export const changeTheme = () => {
5 | return {
6 | type: actionTypes.CHANGE_THEME
7 | }
8 | }
--------------------------------------------------------------------------------
/src/store/reducers/AuthenticationReducer/AuthenticationReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/AuthenticationActionTypes';
2 |
3 | const initialAuthenticationState = {
4 | steam: {
5 | loggedIn: null,
6 | id: null,
7 | apiKey: null,
8 | profile: null,
9 | ownedGames: null,
10 | },
11 | google: {
12 | loggedIn: false,
13 | googleClientReady: false,
14 | profile: null,
15 | },
16 | itad: {
17 | map: null,
18 | },
19 | setupComplete: false,
20 | spreadsheetId: null,
21 | currentSpreadsheetId: null,
22 | permission: null,
23 | }
24 |
25 | export const authentication_reducer = (state = initialAuthenticationState, action) => {
26 | let newState = null;
27 |
28 | switch (action.type) {
29 | case actionTypes.STEAM_SET_ID:
30 | return {
31 | ...state,
32 | steam: {
33 | ...state.steam,
34 | id: action.payload,
35 | }
36 | }
37 | case actionTypes.STEAM_SET_API_KEY:
38 | return {
39 | ...state,
40 | steam: {
41 | ...state.steam,
42 | apiKey: action.payload,
43 | }
44 | }
45 | case actionTypes.STEAM_SET_PROFILE:
46 | return {
47 | ...state,
48 | steam: {
49 | ...state.steam,
50 | profile: action.payload,
51 | }
52 | }
53 | case actionTypes.STEAM_LOGGED:
54 | if (action.payload) {
55 | newState = {
56 | ...state,
57 | steam: {
58 | ...state.steam,
59 | loggedIn: action.payload,
60 | }
61 | }
62 | } else {
63 | newState = {
64 | ...state,
65 | steam: {
66 | ...initialAuthenticationState.steam,
67 | loggedIn: action.payload,
68 | }
69 | }
70 | }
71 |
72 | if (action.payload === true) {
73 | localStorage.setItem('steam', JSON.stringify(newState.steam))
74 | } else {
75 | localStorage.removeItem('steam')
76 | }
77 |
78 | return newState
79 | case actionTypes.STEAM_LOAD:
80 | return {
81 | ...state,
82 | steam: action.payload
83 | }
84 | case actionTypes.STEAM_SET_OWNED_GAMES:
85 | newState = {
86 | ...state,
87 | steam: {
88 | ...state.steam,
89 | ownedGames: action.payload
90 | }
91 | }
92 |
93 | localStorage.setItem('steam', JSON.stringify(newState.steam))
94 |
95 | return {
96 | ...state,
97 | steam: {
98 | ...state.steam,
99 | ownedGames: action.payload
100 | }
101 | }
102 | case actionTypes.ITAD_SET_MAP:
103 | newState = {
104 | ...state,
105 | itad: {
106 | ...state.itad,
107 | map: action.payload
108 | }
109 | }
110 |
111 | localStorage.setItem('itad', JSON.stringify(newState.itad))
112 |
113 | return {
114 | ...state,
115 | itad: {
116 | ...state.itad,
117 | map: action.payload
118 | }
119 | }
120 | case actionTypes.GOOGLE_LOGGED_OUT:
121 | return {
122 | ...state,
123 | google: {
124 | googleClientReady: true,
125 | loggedIn: false,
126 | profile: null
127 | }
128 | }
129 | case actionTypes.GOOGLE_LOGGED_IN:
130 | return {
131 | ...state,
132 | google: {
133 | ...state.google,
134 | loggedIn: true,
135 | profile: action.payload
136 | }
137 | }
138 | case actionTypes.GOOGLE_CLIENT_READY:
139 | return {
140 | ...state,
141 | google: {
142 | ...state.google,
143 | googleClientReady: action.payload,
144 | }
145 | }
146 | case actionTypes.SPREADSHEET_SET_ID:
147 | newState = {
148 | ...state,
149 | spreadsheetId: action.payload
150 | }
151 |
152 | localStorage.setItem('spreadsheetId', action.payload)
153 | return newState
154 | case actionTypes.SET_CURRENT_SPREADSHEET_ID:
155 | return {
156 | ...state,
157 | currentSpreadsheetId: action.payload
158 | }
159 | case actionTypes.SET_CURRENT_SHEET_ID:
160 | return {
161 | ...state,
162 | currentSheetId: action.payload
163 | }
164 | case actionTypes.SET_UP_COMPLETE:
165 | return {
166 | ...state,
167 | setupComplete: action.payload
168 | }
169 | case actionTypes.SET_SPREADSHEET_PERMISSION:
170 | return {
171 | ...state,
172 | permission: action.payload
173 | }
174 | default:
175 | return state;
176 | }
177 | }
--------------------------------------------------------------------------------
/src/store/reducers/AuthenticationReducer/index.js:
--------------------------------------------------------------------------------
1 | import { authentication_reducer } from "./AuthenticationReducer";
2 |
3 | export default authentication_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/FilterReducer/FilterReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/FilterActionTypes';
2 | import _ from 'lodash';
3 |
4 | // Filter Reducer
5 | const initialFiltersState = []
6 |
7 | export const filters_reducer = (state = initialFiltersState, action) => {
8 | switch (action.type) {
9 | case actionTypes.ADD_FILTER:
10 | const oldFilters = state.filter(filter => { return filter.key !== action.payload.key });
11 |
12 | return oldFilters.length > 0
13 | ? _.concat(oldFilters, action.payload)
14 | : [action.payload];
15 | case actionTypes.REMOVE_FILTER:
16 | return state.reduce((result, filter) => {
17 | return filter.key === action.payload.key
18 | ? filter.values.length === 1
19 | ? result
20 | : result.concat([{
21 | key: action.payload.key,
22 | values: filter.values.filter(filterValue => { return filterValue !== action.payload.value }),
23 | id: action.payload.id,
24 | }])
25 | : result.concat(filter)
26 | }, [])
27 | default:
28 | return state;
29 | }
30 | }
--------------------------------------------------------------------------------
/src/store/reducers/FilterReducer/index.js:
--------------------------------------------------------------------------------
1 | import { filters_reducer } from "./FilterReducer";
2 |
3 | export default filters_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/ImportReducer/ImportReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/ImportActionTypes';
2 |
3 | // Import Reducer
4 | const initialImportState = {
5 | headers: null
6 | }
7 |
8 | export const import_reducer = (state = initialImportState, action) => {
9 | switch (action.type) {
10 | case actionTypes.SET_IMPORTED_HEADERS:
11 | return {
12 | ...state,
13 | headers: action.payload
14 | }
15 | case actionTypes.SET_IMPORTED_HEADER:
16 | return {
17 | ...state,
18 | headers: {
19 | ...state.headers,
20 | [action.payload.label]: action.payload
21 | }
22 | }
23 | default:
24 | return state;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/store/reducers/ImportReducer/index.js:
--------------------------------------------------------------------------------
1 | import { import_reducer } from "./ImportReducer";
2 |
3 | export default import_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/StatisticsReducer/StatisticsReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/StatisticsActionsTypes';
2 |
3 | // Statistics Reducer
4 | const initialStatisticsState = {
5 | spreadsheetData: null,
6 | charts: null,
7 | };
8 |
9 | export const statistics_reducer = (state = initialStatisticsState, action) => {
10 | switch (action.type) {
11 | case actionTypes.LOAD_STATISTICS_SPREADSHEET:
12 | return {
13 | ...state,
14 | spreadsheetData: action.payload,
15 | };
16 | case actionTypes.CLEAR_STATISTICS_SPREADSHEET:
17 | localStorage.removeItem('statisticsSpreadsheet');
18 |
19 | return {
20 | ...state,
21 | spreadsheetData: null,
22 | };
23 | case actionTypes.LOAD_STATISTICS_CHARTS:
24 | return {
25 | ...state,
26 | charts: action.payload,
27 | };
28 | case actionTypes.CLEAR_STATISTICS_CHARTS:
29 | localStorage.removeItem('statisticsCharts');
30 |
31 | return {
32 | ...state,
33 | charts: null,
34 | };
35 | case actionTypes.RESET_STATISTICS_STORAGE:
36 | localStorage.removeItem('statisticsSpreadsheet');
37 | localStorage.removeItem('statisticsCharts');
38 |
39 | return {
40 | ...state,
41 | spreadsheetData: null,
42 | charts: null,
43 | };
44 | default:
45 | return state;
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/src/store/reducers/StatisticsReducer/index.js:
--------------------------------------------------------------------------------
1 | import { statistics_reducer } from "./StatisticsReducer";
2 |
3 | export default statistics_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/TableReducer/TableReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/TableActionTypes';
2 | import _ from 'lodash';
3 |
4 | // Table Reducer
5 | const initialTableState = {
6 | headers: {},
7 | rows: [],
8 | changes: {},
9 | reset: {
10 | limit: false,
11 | offset: false,
12 | filters: false,
13 | orderBy: false,
14 | },
15 | orderBy: { sort: 'ID', asc: false },
16 | reload: false,
17 | pageSize: 24,
18 | isEmpty: true,
19 | showShareModal: false,
20 | }
21 |
22 | export const table_reducer = (state = initialTableState, action) => {
23 | switch (action.type) {
24 | case actionTypes.SET_CURRENT_ROWS:
25 | return {
26 | ...state,
27 | rows: action.payload
28 | }
29 | case actionTypes.UPDATE_ROW:
30 | return {
31 | ...state,
32 | rows: state.rows.reduce((result, row, index) =>
33 | index === action.payload.index
34 | ? [...result, action.payload.row]
35 | : [...result, row], [])
36 | }
37 | case actionTypes.CHANGE_PAGE_SIZE:
38 | return {
39 | ...state,
40 | orderBy: action.payload
41 | }
42 | case actionTypes.CHANGE_ORDER_BY:
43 | return {
44 | ...state,
45 | orderBy: action.payload
46 | }
47 | case actionTypes.RELOAD_TABLE:
48 | return {
49 | ...state,
50 | reload: action.payload
51 | }
52 | case actionTypes.RESET_TABLE_PARAMS:
53 | return {
54 | ...state,
55 | reset: action.payload.reduce((result, paramToReset) => (
56 | {
57 | ...result,
58 | [paramToReset]: !result[paramToReset]
59 | }
60 | ), state.reset)
61 | }
62 | case actionTypes.ADD_HEADERS:
63 | return {
64 | ...state,
65 | headers: action.payload
66 | }
67 | case actionTypes.REMOVE_HEADERS:
68 | return {
69 | ...state,
70 | headers: {}
71 | }
72 | case actionTypes.SET_NEW_ROW_CHANGE:
73 | return {
74 | ...state,
75 | changes: {
76 | ...state.changes,
77 | [action.payload.id]: action.payload.row
78 | }
79 | }
80 | case actionTypes.REMOVE_NEW_ROW_CHANGE:
81 | return {
82 | ...state,
83 | changes: _.omit(state.changes, action.payload)
84 | }
85 | case actionTypes.SET_IS_TABLE_EMPTY:
86 | return {
87 | ...state,
88 | isEmpty: action.payload
89 | }
90 | case actionTypes.SHOW_SHARE_MODAL:
91 | return {
92 | ...state,
93 | showShareModal: action.payload
94 | }
95 | default:
96 | return state;
97 | }
98 | }
--------------------------------------------------------------------------------
/src/store/reducers/TableReducer/index.js:
--------------------------------------------------------------------------------
1 | import { table_reducer } from "./TableReducer";
2 |
3 | export default table_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/ThemeReducer/ThemeReducer.js:
--------------------------------------------------------------------------------
1 | import * as actionTypes from '../../actionTypes/ThemeActionTypes';
2 |
3 | // Theme Reducer
4 | const initialThemeState = {
5 | selected: "light",
6 | light: {
7 | name: "light",
8 | foreground: "#000000",
9 | background: "#eeeeee"
10 | },
11 | dark: {
12 | name: "dark",
13 | foreground: "#ffffff",
14 | background: "#222222"
15 | }
16 | }
17 |
18 | export const theme_reducer = (state = initialThemeState, action) => {
19 | switch (action.type) {
20 | case actionTypes.CHANGE_THEME:
21 | return {
22 | ...state,
23 | selected: state.selected === "light" ? "dark" : "light"
24 | }
25 | default:
26 | return state;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/store/reducers/ThemeReducer/index.js:
--------------------------------------------------------------------------------
1 | import { theme_reducer } from "./ThemeReducer";
2 |
3 | export default theme_reducer;
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import authentication_reducer from './AuthenticationReducer';
3 | import filters_reducer from './FilterReducer';
4 | import import_reducer from './ImportReducer';
5 | import { statistics_reducer } from './StatisticsReducer/StatisticsReducer';
6 | import table_reducer from './TableReducer';
7 | import theme_reducer from './ThemeReducer';
8 |
9 | const rootReducer = combineReducers({
10 | filters: filters_reducer,
11 | table: table_reducer,
12 | theme: theme_reducer,
13 | authentication: authentication_reducer,
14 | import: import_reducer,
15 | statistics: statistics_reducer,
16 | });
17 |
18 | export default rootReducer;
--------------------------------------------------------------------------------
/src/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension';
3 | import rootReducer from './reducers';
4 |
5 | const store = createStore(rootReducer, composeWithDevTools());
6 |
7 | export default store;
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Verdana, Geneva, sans-serif;
5 | }
6 |
7 | .search-fluid-input .ui.icon.input {
8 | width: 100%;
9 | }
10 |
11 | .search-fluid-input .category {
12 | display: block !important;
13 | }
14 |
15 | .pointer {
16 | cursor: pointer;
17 | }
18 |
19 | .full-width {
20 | width: 100%;
21 | }
22 |
23 | .cursor-auto {
24 | cursor: auto !important;
25 | }
26 |
27 | .visibility-hidden {
28 | visibility: hidden;
29 | }
30 |
31 | .__dropdown {
32 | }
33 |
34 | .__dropdown > .dropdown {
35 | display: flex;
36 | width: 100%;
37 | align-items: center;
38 | justify-content: flex-start;
39 | }
40 | .__dropdown > .dropdown > input {
41 | cursor: pointer !important;
42 | }
43 |
44 | .__dropdown > .dropdown > .text {
45 | display: flex;
46 | cursor: pointer !important;
47 | flex-grow: 1;
48 | }
49 |
50 | .__dropdown > .dropdown > .icon {
51 | color: black !important;
52 | display: flex;
53 | cursor: pointer !important;
54 | }
55 |
56 | .__dropdown > .dropdown > .menu {
57 | width: 100%;
58 | }
59 |
60 | .contained-image .carousel__image {
61 | background-size: contain !important;
62 | background-position: center !important;
63 | background-repeat: no-repeat !important;
64 | }
65 |
66 | .ui.fullscreen.modal {
67 | left: auto !important;
68 | }
69 |
70 | .segment-no-last-child-bottom-margin.ui.segment:last-child {
71 | /* margin-bottom: 1rem; */
72 | }
73 |
74 | .no-margin {
75 | margin: 0 !important;
76 | }
77 |
78 | .show-messages > .ui.error.message {
79 | display: block !important;
80 | }
81 |
82 | .filters-search-column {
83 | width: 25% !important;
84 | height: 100%;
85 | display: flex;
86 | justify-content: center;
87 | }
88 |
89 | .ui.simple.active.dropdown > .menu,
90 | .ui.simple.dropdown:hover > .menu.actions-menu {
91 | top: -200% !important;
92 | left: 100%;
93 | }
94 |
95 | .gameinfo-with-background {
96 | background: transparent !important;
97 | }
98 | .gameinfo-with-background > .header,
99 | .gameinfo-with-background > .content {
100 | background-color: rgb(29, 40, 57) !important;
101 | }
102 | .gameinfo-with-background.ui.modal > .header,
103 | .gameinfo-with-background .ui.statistic > .label,
104 | .gameinfo-with-background .ui.statistics .statistic > .label,
105 | .gameinfo-with-background .ui.vertical.segment,
106 | .gameinfo-with-background i.close.icon {
107 | color: white !important;
108 | }
109 |
110 | .gameinfo-with-background .carousel .ui.basic.buttons .button {
111 | background-color: white !important;
112 | }
113 |
114 | .white-tabs > .ui.menu > .item {
115 | color: rgba(255, 255, 255, 0.5) !important;
116 | }
117 |
118 | .white-tabs > .ui.menu > .active.item {
119 | color: rgba(255, 255, 255, 0.95) !important;
120 | border-color: rgba(255, 255, 255, 0.95) !important;
121 | }
122 |
123 | .awssld.img-contain img {
124 | object-fit: contain;
125 | }
126 |
127 | .urls-wrapper {
128 | display: flex;
129 | justify-content: space-evenly !important;
130 | }
131 |
132 | .urls-wrapper > .url-icon-wrapper {
133 | display: flex;
134 | align-items: center;
135 | min-width: 1.5em;
136 | min-height: 1.5em;
137 | }
138 |
139 | .google-button {
140 | cursor: pointer;
141 | background-color: rgb(255, 255, 255);
142 | display: inline-flex;
143 | align-items: center;
144 | color: rgba(0, 0, 0, 0.54);
145 | box-shadow: rgba(0, 0, 0, 0.24) 0px 2px 2px 0px,
146 | rgba(0, 0, 0, 0.24) 0px 0px 1px 0px;
147 | padding: 0px;
148 | border-radius: 2px;
149 | border: 1px solid transparent;
150 | font-size: 14px;
151 | font-weight: 500;
152 | font-family: Roboto, sans-serif;
153 | }
154 | .google-button > div {
155 | margin-right: 10px;
156 | background: rgb(255, 255, 255) none repeat scroll 0% 0%;
157 | padding: 10px;
158 | border-radius: 2px;
159 | }
160 | .google-button > span {
161 | padding: 10px;
162 | font-weight: 500;
163 | }
164 |
165 | /* Mobile */
166 |
167 | @media only screen and (max-width: 767px) {
168 | .ui.table thead {
169 | position: unset !important;
170 | top: auto !important;
171 | z-index: auto !important;
172 | }
173 |
174 | .urls-wrapper {
175 | padding: 0.25em 0.75em !important;
176 | justify-content: flex-start !important;
177 | }
178 |
179 | .urls-wrapper > .url-icon-wrapper:not(last-child) {
180 | padding-right: 1em;
181 | }
182 | }
183 |
184 | /* Tablet */
185 |
186 | @media only screen and (min-width: 768px) and (max-width: 991px) {
187 | }
188 |
189 | /* Small Monitor */
190 |
191 | @media only screen and (min-width: 992px) and (max-width: 1199px) {
192 | }
193 |
194 | /* Large Monitor */
195 |
196 | @media only screen and (min-width: 1200px) {
197 | }
198 |
--------------------------------------------------------------------------------