├── .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 | 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 |
12 | 13 | 18 | 19 |
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 | 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 | 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 | 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 | 148 | 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 |
87 | {name} 88 |
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 | 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 |
60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 |
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 | 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 | 76 | 77 | 78 | 79 | 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 |
110 |
111 | Spreadsheet created: Spreadsheet URL 112 |
113 |
114 | Keys-DB Url: Keys-DB URL 115 |
116 |
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 |
Settings
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 |
123 | 124 | 141 | { 142 | steam.id !== null && 152 | } 153 | { 154 | /* */ 160 | } 161 | 162 | 163 | 164 |
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 | 51 | 52 | 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 | 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 | 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 | 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 | //
17 | // 18 | // 19 | // 20 | // 21 | // 22 | // 23 | // 24 | // 25 | //
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 | 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 | --------------------------------------------------------------------------------