├── .env
├── .env.production
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── modules.xml
├── prettier.xml
├── scream-db.iml
└── vcs.xml
├── .prettierignore
├── .prettierrc.json
├── LICENSE.txt
├── README.md
├── codegen.yml
├── firebase.json
├── graphql
└── queries.graphql
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo512.png
├── manifest.json
└── robots.txt
├── schema.graphql
├── src
├── App.tsx
├── components
│ ├── appbar
│ │ ├── LanguagePicker.tsx
│ │ └── ScreamAppBar.tsx
│ ├── footer
│ │ └── ScreamFooter.tsx
│ ├── games
│ │ ├── SortBySelect.tsx
│ │ └── SortDirButton.tsx
│ ├── offers
│ │ └── OfferTypeFilter.tsx
│ ├── router
│ │ └── ScreamSwitch.tsx
│ ├── settings
│ │ └── Settings.tsx
│ ├── skeletons
│ │ ├── GameCardSkeleton.tsx
│ │ └── OfferRowSkeleton.tsx
│ ├── util
│ │ ├── CustomSelect.tsx
│ │ ├── Link.tsx
│ │ ├── OverflowBody.tsx
│ │ ├── OverflowText.tsx
│ │ ├── PaginatedContainer.tsx
│ │ ├── ResponsiveBox.tsx
│ │ ├── ResponsiveContainer.tsx
│ │ ├── SadFace.tsx
│ │ └── SearchBar.tsx
│ ├── view-items
│ │ ├── GameCard.tsx
│ │ └── OfferRow.tsx
│ └── views
│ │ └── TableView.tsx
├── context
│ ├── ContextProviders.tsx
│ ├── keywords.ts
│ └── language.ts
├── generated
│ └── graphql.ts
├── hooks
│ ├── locale.ts
│ └── screen-size.ts
├── index.css
├── index.tsx
├── logo.svg
├── md
│ ├── home_en.md
│ ├── home_es.md
│ ├── home_ru.md
│ └── home_zh.md
├── pages
│ ├── Games.tsx
│ ├── Home.tsx
│ └── Offers.tsx
├── react-app-env.d.ts
├── reportWebVitals.ts
└── util
│ ├── locale.ts
│ ├── log.ts
│ ├── paths.ts
│ ├── query.ts
│ ├── storage.ts
│ ├── types.ts
│ └── typings.d.ts
├── tsconfig.json
├── workers
└── epic-cors-proxy.js
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=$npm_package_version
2 | REACT_APP_CORS_PROXY=https://epic-cors-proxy.acidicoala.workers.dev/
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_CORS_PROXY=https://epic-cors-proxy.acidicoala.workers.dev/
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more browse ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .eslintcache
26 | debug.log
27 | .env.development
28 | .firebase
29 | .firebase*
30 | firebase-debug.log
31 |
32 | # Workers
33 | workers-site
34 | dist
35 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /../../../../../:\Dev\WebStormProjects\scream-db\.idea/dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/scream-db.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .idea
2 | graphql
3 | node_modules
4 | public
5 | workers
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 100
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (C) 2022 by Acidicoala
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose
4 | with or without fee is hereby granted.
5 |
6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
8 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
10 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
11 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
12 | THIS SOFTWARE.
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🐨 ScreamDB
2 |
3 | ### Welcome to the ScreamDB repository.
4 |
5 | For user-friendly introduction, please check out the web app's [home page](https://scream-db.web.app/). This document is meant for developers.
6 |
7 | ## 🚀 App architecture
8 |
9 | This web was developed and hosted using great technologies such as:
10 |
11 | * [🆁eact](https://reactjs.org/)
12 | * [🆃ypescript](https://www.typescriptlang.org/)
13 | * [🅼aterialUI](https://material-ui.com/)
14 | * [🅶raphQL Code Generator](https://graphql-code-generator.com/)
15 | * [🅲loudflare Workers](https://workers.cloudflare.com/)
16 | * [🅵irebase](https://firebase.google.com/)
17 |
18 | ## 🏢 Hosting
19 |
20 | The web app is hosted on 🔥 Firebase at
21 |
22 | ## 🔐 The CORS issue
23 |
24 | Modern browsers enforce strict [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) policy. That means the web app cannot directly make requests to the Epic Games' [GraphQL endpoint](https://www.epicgames.com/graphql). Furthermore, the endpoint has a whitelist of valid `Referer` header values, which unfortunately is not possible to set using browser's JavaScript. To overcome these issues I have deployed a simple CORS proxy script on the Cloudflare Workers platform. It redirects all request to the actual GraphQL endpoint but modifies the response header [`Access-Control-Allow-Origin`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) with the domain of this web app. The source of the script is available in this repository: [epic-cors-proxy](./workers/epic-cors-proxy.js). The project is configured to use this proxy in development and production, but it is possible to override them via `.env` files.
25 |
26 | ## 🌐 Localization
27 |
28 | Currently, the following languages are supported:
29 | * English
30 | * Spanish (Credit to @g-yui)
31 | * Russian
32 | * Simplified Chinese (Credit to @Citrinae-Lime)
33 |
34 | The web app localization is defined in [src/util/locale.ts](./src/util/locale.ts).
35 |
36 | Furthermore, home page is rendering localized Markdown documents located at [src/md](./src/md).
37 |
38 | If you wish to contribute a translation for another language, you are free to submit a pull request.
39 |
40 | ## 📜 Available Scripts
41 |
42 | In the project directory, you can run the following commands:
43 |
44 | | Command | Documentation |
45 | |---------------------|-------------------------------------------------|
46 | | `yarn start` | [start @ CRA docs] |
47 | | `yarn build` | [build @ CRA docs] |
48 | | `yarn deploy` | [deploy @ Firebase docs] |
49 | | `yarn generate-sdk` | [graphql-codegen @ GraphQL Code Generator docs] |
50 |
51 | The commands above assume that the corresponding CLI tools have been installed and configured.
52 |
53 | ## 📄 License
54 | This software is licensed under [Zero Clause BSD](https://choosealicense.com/licenses/0bsd/) license, terms of which are available in [LICENSE.txt](./LICENSE.txt).
55 |
56 | [start @ CRA docs]: https://github.com/facebook/create-react-app#npm-start-or-yarn-start
57 | [build @ CRA docs]: https://github.com/facebook/create-react-app#npm-run-build-or-yarn-build
58 | [deploy @ Firebase docs]: https://firebase.google.com/docs/cli#deployment
59 | [graphql-codegen @ GraphQL Code Generator docs]: https://graphql-code-generator.com/docs/plugins/typescript-graphql-request
60 |
--------------------------------------------------------------------------------
/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: "schema.graphql"
3 | documents: "graphql/**/*.graphql"
4 | generates:
5 | src/generated/graphql.ts:
6 | plugins:
7 | - typescript
8 | - typescript-operations
9 | - typescript-resolvers
10 | - typescript-graphql-request
11 | config:
12 | fetcher: graphql-request
13 | scalars:
14 | DateTime: Date
15 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "rewrites": [
10 | {
11 | "source": "**",
12 | "destination": "/index.html"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/graphql/queries.graphql:
--------------------------------------------------------------------------------
1 | query searchGames($keywords: String!, $sortBy: String, $sortDir: String) {
2 | Catalog {
3 | searchStore(
4 | category: "games/edition/base"
5 | count: 1000
6 | keywords: $keywords
7 | sortBy: $sortBy
8 | sortDir: $sortDir
9 | ) {
10 | elements {
11 | id
12 | title
13 | namespace
14 | creationDate
15 | items {
16 | id
17 | namespace
18 | }
19 | keyImages {
20 | type
21 | url
22 | }
23 | }
24 | }
25 | }
26 | }
27 |
28 | query searchOffers($namespace: String!) {
29 | Catalog {
30 | catalogOffers(
31 | namespace: $namespace
32 | params: {
33 | count: 1000,
34 | }
35 | ) {
36 | elements {
37 | id
38 | title
39 | offerType
40 | items {
41 | id
42 | }
43 | keyImages {
44 | type
45 | url
46 | }
47 | }
48 | }
49 | # Get the game title as well
50 | searchStore(category: "games/edition/base", namespace: $namespace){
51 | elements {
52 | title
53 | catalogNs{
54 | mappings(pageType: "productHome"){
55 | pageSlug
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scream-db",
3 | "version": "1.3.0",
4 | "private": true,
5 | "homepage": "/",
6 | "type": "module",
7 | "dependencies": {
8 | "@graphql-codegen/cli": "^1.21.8",
9 | "@graphql-codegen/typescript": "1.23.0",
10 | "@graphql-codegen/typescript-graphql-request": "^3.0.0",
11 | "@graphql-codegen/typescript-operations": "1.18.4",
12 | "@graphql-codegen/typescript-resolvers": "^1.18.1",
13 | "@material-ui/core": "^4.11.2",
14 | "@material-ui/icons": "^4.11.2",
15 | "@material-ui/lab": "^4.0.0-alpha.57",
16 | "@testing-library/jest-dom": "^5.11.4",
17 | "@testing-library/react": "^12.0.0",
18 | "@testing-library/user-event": "^13.2.1",
19 | "@types/jest": "^26.0.15",
20 | "@types/node": "^16.4.10",
21 | "@types/react": "^17.0.0",
22 | "@types/react-dom": "^17.0.0",
23 | "@types/react-router-dom": "^5.1.7",
24 | "@types/react-syntax-highlighter": "^13.5.0",
25 | "graphql": "^15.4.0",
26 | "graphql-request": "^3.4.0",
27 | "material-ui-popup-state": "^1.7.1",
28 | "react": "^17.0.1",
29 | "react-animate-height": "^2.0.23",
30 | "react-dom": "^17.0.1",
31 | "react-markdown": "^6.0.3",
32 | "react-router-dom": "^5.2.0",
33 | "react-scripts": "4.0.3",
34 | "react-syntax-highlighter": "^15.4.3",
35 | "typescript": "^4.1.3",
36 | "web-vitals": "^2.1.0"
37 | },
38 | "devDependencies": {
39 | "firebase-tools": "^10.2.1",
40 | "prettier": "2.5.1"
41 | },
42 | "scripts": {
43 | "start": "react-scripts start",
44 | "build": "react-scripts build",
45 | "deploy": "firebase deploy",
46 | "generate-sdk": "graphql-codegen --config codegen.yml"
47 | },
48 | "eslintConfig": {
49 | "extends": [
50 | "react-app",
51 | "react-app/jest"
52 | ],
53 | "plugins": [
54 | "react-hooks"
55 | ],
56 | "overrides": [
57 | {
58 | "files": [
59 | "**/*.ts?(x)"
60 | ],
61 | "rules": {
62 | "react-hooks/exhaustive-deps": "off"
63 | }
64 | }
65 | ]
66 | },
67 | "browserslist": {
68 | "production": [
69 | ">0.2%",
70 | "not dead",
71 | "not op_mini all"
72 | ],
73 | "development": [
74 | "last 1 chrome version",
75 | "last 1 firefox version",
76 | "last 1 safari version"
77 | ]
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acidicoala/ScreamDB/a16a6c9c868e9afd27181d1865e52929d5d756c5/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ScreamDB
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acidicoala/ScreamDB/a16a6c9c868e9afd27181d1865e52929d5d756c5/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "ScreamDB",
3 | "name": "ScreamDB",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#2F2F2F",
14 | "background_color": "#2F2F2F"
15 | }
16 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/schema.graphql:
--------------------------------------------------------------------------------
1 | schema {
2 | query: Query
3 | }
4 |
5 | type Query {
6 | Catalog: CatalogQuery
7 | }
8 |
9 | type CatalogQuery {
10 | searchStore(category: String, count: Int, namespace: String, keywords: String, sortBy: String, sortDir: String): Elements!
11 | catalogOffers(namespace: String!, params: CatalogOffersParams): Elements!
12 | }
13 |
14 | input CatalogOffersParams {
15 | count: Int
16 | }
17 |
18 | type Elements {
19 | elements: [Element!]!
20 | }
21 |
22 | type Mapping{
23 | pageSlug: String!
24 | }
25 | type CatalogNs {
26 | mappings(pageType: String): [Mapping!]!
27 | }
28 |
29 | type Element {
30 | id: String!
31 | title: String!
32 | namespace: String!
33 | offerType: OfferType!
34 | items: [Item!]!
35 | keyImages: [KeyImage!]!
36 | creationDate: String!
37 | catalogNs: CatalogNs!
38 | }
39 |
40 | enum OfferType {
41 | ADD_ON
42 | BASE_GAME
43 | BUNDLE
44 | EDITION
45 | DLC
46 | OTHERS
47 | UNLOCKABLE
48 | }
49 |
50 | type Item {
51 | id: String!
52 | title: String!
53 | namespace: String!
54 | }
55 |
56 | type KeyImage {
57 | type: KeyImageType!
58 | url: String
59 | }
60 |
61 | enum KeyImageType {
62 | OfferImageTall
63 | OfferImageWide
64 | Thumbnail
65 | CodeRedemption_340x440
66 | DieselStoreFrontTall
67 | DieselStoreFrontWide
68 | }
69 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | createTheme,
4 | CssBaseline,
5 | responsiveFontSizes,
6 | ThemeOptions,
7 | ThemeProvider,
8 | } from "@material-ui/core";
9 | import { ScreamAppBar } from "./components/appbar/ScreamAppBar";
10 | import { ScreamSwitch } from "./components/router/ScreamSwitch";
11 | import { BrowserRouter as Router } from "react-router-dom";
12 | import { ContextProviders } from "./context/ContextProviders";
13 | import { OverflowBody } from "./components/util/OverflowBody";
14 | import { ResponsiveContainer } from "./components/util/ResponsiveContainer";
15 |
16 | const theme = responsiveFontSizes(
17 | createTheme({
18 | palette: {
19 | type: "dark",
20 | primary: {
21 | main: "#2e7d32",
22 | contrastText: "#FFF",
23 | },
24 | secondary: {
25 | main: "#FFF",
26 | },
27 | background: {
28 | default: "#2F2F2F",
29 | },
30 | },
31 | } as ThemeOptions)
32 | );
33 |
34 | function App() {
35 | return (
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {/**/}
47 |
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | export default App;
56 |
--------------------------------------------------------------------------------
/src/components/appbar/LanguagePicker.tsx:
--------------------------------------------------------------------------------
1 | import { rawLocale } from "../../util/locale";
2 | import { CustomSelect } from "../util/CustomSelect";
3 | import React from "react";
4 | import { useLanguage } from "../../context/language";
5 | import { KeyboardArrowDown, Translate } from "@material-ui/icons";
6 | import { useLocale } from "../../hooks/locale";
7 | import { useXS } from "../../hooks/screen-size";
8 | import { ValidLanguage } from "../../util/types";
9 |
10 | export function LanguagePicker() {
11 | const { setLang } = useLanguage();
12 | const { locale } = useLocale();
13 | const xs = useXS();
14 |
15 | const languages = [
16 | { key: "en", text: rawLocale.lang.en },
17 | { key: "es", text: rawLocale.lang.es },
18 | { key: "ru", text: rawLocale.lang.ru },
19 | { key: "zh", text: rawLocale.lang.zh },
20 | ];
21 |
22 | return (
23 | }
26 | endIcon={}
27 | items={languages}
28 | onItemSelect={(item) => setLang(item.key as ValidLanguage)}
29 | children={!xs && locale.lang}
30 | />
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/appbar/ScreamAppBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | AppBar,
4 | Box,
5 | Button,
6 | ButtonGroup,
7 | Container,
8 | createStyles,
9 | Divider,
10 | Hidden,
11 | IconButton,
12 | makeStyles,
13 | Toolbar,
14 | Typography,
15 | } from "@material-ui/core";
16 | import { maxWidth } from "../../util/storage";
17 | import { path } from "../../util/paths";
18 | import { useLocale } from "../../hooks/locale";
19 | import { useXS } from "../../hooks/screen-size";
20 | import { LanguagePicker } from "./LanguagePicker";
21 | import { Link, useHistory } from "react-router-dom";
22 | import AnimateHeight from "react-animate-height";
23 | import { Menu } from "@material-ui/icons";
24 | import { ScreamLink } from "../util/Link";
25 | import { useKeywords } from "../../context/keywords";
26 | import { SearchBar } from "../util/SearchBar";
27 |
28 | const useStyles = makeStyles(() =>
29 | createStyles({
30 | toolbar: {
31 | padding: 0,
32 | },
33 | })
34 | );
35 |
36 | export function ScreamAppBar() {
37 | const history = useHistory();
38 | const { locale } = useLocale();
39 | const classes = useStyles();
40 | const { setKeywords } = useKeywords();
41 | const xs = useXS();
42 |
43 | const [open, setOpen] = useState(false);
44 |
45 | const NavButtons = () => (
46 |
47 | {
48 |
56 | );
57 |
58 | function onSearch(query: string) {
59 | if (history.location.pathname !== path.to.games) history.push(path.to.games);
60 | setKeywords(query);
61 | }
62 |
63 | return (
64 |
65 |
66 |
67 |
68 | setOpen(!open)} children={} />
69 |
70 |
71 | ScreamDB} />
72 |
73 |
74 | } />
75 |
76 | }
79 | />
80 |
81 |
82 |
83 | {/* */}
84 | {/* Reserved for future use*/}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | } />
94 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/footer/ScreamFooter.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@material-ui/core";
2 | import { useLocale } from "../../hooks/locale";
3 |
4 | export function ScreamFooter() {
5 | const { locale } = useLocale();
6 | const versionNumber = process.env.REACT_APP_VERSION;
7 |
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/games/SortBySelect.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { KeyboardArrowDown } from "@material-ui/icons";
3 | import { CustomSelect } from "../util/CustomSelect";
4 | import { useLocale } from "../../hooks/locale";
5 | import { writeProp } from "../../util/storage";
6 |
7 | export enum SortOption {
8 | RELEVANCE = "",
9 | TITLE = "title",
10 | CREATION_DATE = "creationDate",
11 | RELEASE_DATE = "releaseDate",
12 | PC_RELEASE_DATE = "pcReleaseDate",
13 | CURRENT_PRICE = "currentPrice",
14 | }
15 |
16 | export function SortBySelect(props: {
17 | sortBy: SortOption;
18 | setSortBy: (option: SortOption) => void;
19 | }) {
20 | const { locale } = useLocale();
21 | const { sortBy, setSortBy } = props;
22 |
23 | const sortOptions = [
24 | { key: SortOption.RELEVANCE, text: locale.sort_relevance },
25 | { key: SortOption.TITLE, text: locale.sort_title },
26 | { key: SortOption.CREATION_DATE, text: locale.sort_creation_date },
27 | { key: SortOption.RELEASE_DATE, text: locale.sort_new_release },
28 | { key: SortOption.PC_RELEASE_DATE, text: locale.sort_release_date },
29 | { key: SortOption.CURRENT_PRICE, text: locale.sort_current_price },
30 | ];
31 |
32 | return (
33 | }
38 | items={sortOptions}
39 | onItemSelect={(item) => {
40 | setSortBy(item.key);
41 | writeProp("sort_games_by", item.key);
42 | }}
43 | children={sortOptions.find((it) => it.key === sortBy)?.text}
44 | />
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/games/SortDirButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button } from "@material-ui/core";
3 | import { useLocale } from "../../hooks/locale";
4 | import { ArrowDownward, ArrowUpward } from "@material-ui/icons";
5 | import { ValidSortDirection } from "../../util/types";
6 | import { writeProp } from "../../util/storage";
7 |
8 | export function SortDirButton(props: {
9 | sortDir: ValidSortDirection;
10 | setSortDir: (dir: ValidSortDirection) => void;
11 | }) {
12 | const { sortDir, setSortDir } = props;
13 | const [isAscending, setIsAscending] = useState(sortDir === "ASC");
14 | const { locale } = useLocale();
15 |
16 | return (
17 | : }
20 | children={isAscending ? locale.ascending : locale.descending}
21 | onClick={() => {
22 | const newDirection = isAscending ? "DESC" : "ASC";
23 | setSortDir(newDirection);
24 | writeProp("sort_games_dir", newDirection);
25 | setIsAscending(!isAscending);
26 | }}
27 | />
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/offers/OfferTypeFilter.tsx:
--------------------------------------------------------------------------------
1 | import {Box, Button, ButtonGroup, Checkbox, Divider, FormControlLabel, FormGroup, Popover} from "@material-ui/core";
2 | import {bindTrigger, usePopupState} from "material-ui-popup-state/hooks";
3 | import {bindPopover} from "material-ui-popup-state";
4 | import React from "react";
5 | import {OfferType} from "../../generated/graphql";
6 | import {useLocale} from "../../hooks/locale";
7 |
8 | export function OfferTypeFilter(props:{
9 | offerTypeFilters: Record,
10 | setOfferTypeFilters: (typeFilters: Record) => void
11 | }){
12 | const {offerTypeFilters,setOfferTypeFilters} = props
13 | const {locale} = useLocale()
14 | const popupState = usePopupState({
15 | variant: 'popover',
16 | popupId: 'filter-offer-type',
17 | })
18 |
19 | return (
20 | <>
21 |
27 |
38 |
39 | {
40 | Object.entries(offerTypeFilters).map(([type, enabled]) =>
41 |
48 | setOfferTypeFilters({
49 | ...offerTypeFilters,
50 | [type]: event.target.checked
51 | })
52 | }/>}
53 | label={type}
54 | />
55 | )}
56 |
57 |
58 |
59 |
60 |
77 |
78 |
79 | >
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/router/ScreamSwitch.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Switch } from "react-router-dom";
3 | import { path } from "../../util/paths";
4 | import { Home } from "../../pages/Home";
5 | import { Games } from "../../pages/Games";
6 | import { SadFace } from "../util/SadFace";
7 | import { useLocale } from "../../hooks/locale";
8 | import { Offers } from "../../pages/Offers";
9 |
10 | export function ScreamSwitch() {
11 | const { locale } = useLocale();
12 |
13 | return (
14 |
15 | } />
16 | } />
17 | } />
18 | } />
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Box,
4 | createStyles,
5 | Divider,
6 | Drawer,
7 | IconButton,
8 | makeStyles,
9 | Typography,
10 | } from "@material-ui/core";
11 | import SettingsIcon from "@material-ui/icons/Settings";
12 | import { useLocale } from "../../hooks/locale";
13 | import { Close } from "@material-ui/icons";
14 |
15 | const useStyles = makeStyles(({ breakpoints, spacing }) =>
16 | createStyles({
17 | settings: {
18 | width: "100%",
19 | maxWidth: "100%",
20 | [breakpoints.up("sm")]: {
21 | maxWidth: spacing(45),
22 | },
23 | },
24 | })
25 | );
26 |
27 | export function Settings() {
28 | const { settings } = useStyles();
29 | const [open, setOpen] = useState(false);
30 | const { locale } = useLocale();
31 |
32 | return (
33 | <>
34 | setOpen(true)}>
35 |
36 |
37 | setOpen(false)}
44 | >
45 | <>
46 |
47 |
55 | setOpen(false)}>
56 |
57 |
58 |
59 |
60 | >
61 |
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/skeletons/GameCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from "react";
2 | import { Skeleton } from "@material-ui/lab";
3 | import { Box } from "@material-ui/core";
4 |
5 | export function GameCardSkeleton(props?: { style?: CSSProperties }) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/skeletons/OfferRowSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from "react";
2 | import { Skeleton } from "@material-ui/lab";
3 | import { TableCell, TableRow } from "@material-ui/core";
4 |
5 | export function OfferRowSkeleton(props?: { style?: CSSProperties }) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/util/CustomSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Menu, MenuItem, Typography } from "@material-ui/core";
2 | import { ButtonProps } from "@material-ui/core/Button";
3 | import { bindMenu, bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
4 | import { Key } from "react";
5 |
6 | export function CustomSelect(
7 | props: {
8 | menuItemClassName?: string;
9 | items?: Array;
10 | onItemSelect?: (item: T) => void;
11 | } & ButtonProps
12 | ) {
13 | const { menuItemClassName, items, onItemSelect, ...buttonProps } = props;
14 | const popupState = usePopupState({ variant: "popover", popupId: null });
15 |
16 | return (
17 | <>
18 |
19 |
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/util/Link.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, LinkProps } from "react-router-dom";
3 | import { createStyles, makeStyles } from "@material-ui/core";
4 |
5 | const useStyles = makeStyles(() =>
6 | createStyles({
7 | link: {
8 | color: "white",
9 | textDecoration: "none",
10 | "&:hover": {
11 | textDecoration: "underline",
12 | },
13 | },
14 | })
15 | );
16 |
17 | export function ScreamLink(props: LinkProps) {
18 | const { link } = useStyles();
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/util/OverflowBody.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from "react";
2 | import { createStyles, makeStyles } from "@material-ui/core";
3 |
4 | const useStyles = makeStyles(({ breakpoints }) =>
5 | createStyles({
6 | body: {
7 | [breakpoints.up("sm")]: {
8 | overflowY: "scroll",
9 | height: `calc(100vh - 64px)`,
10 | },
11 | },
12 | })
13 | );
14 |
15 | export function OverflowBody(props: PropsWithChildren<{}>) {
16 | const { body } = useStyles();
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/util/OverflowText.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, PropsWithChildren } from "react";
2 | import { createStyles, makeStyles } from "@material-ui/core";
3 |
4 | const useStyles = (lines: number) =>
5 | makeStyles(() =>
6 | createStyles({
7 | text: {
8 | overflow: "hidden",
9 | textOverflow: "ellipsis",
10 | display: "-webkit-box",
11 | "-webkit-line-clamp": lines,
12 | "-webkit-box-orient": "vertical",
13 | },
14 | })
15 | )();
16 |
17 | export function OverflowText(
18 | props: PropsWithChildren<{
19 | lines: number;
20 | style: CSSProperties;
21 | }>
22 | ) {
23 | const { lines, style, children } = props;
24 | const { text } = useStyles(lines);
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/util/PaginatedContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren, useRef, useState } from "react";
2 | import { Box, TablePagination } from "@material-ui/core";
3 | import { readProp, writeProp } from "../../util/storage";
4 | import { createTheme, ThemeProvider } from "@material-ui/core/styles";
5 | import { ruRU, enUS, esES, zhCN } from "@material-ui/core/locale";
6 | import { useLanguage } from "../../context/language";
7 |
8 | const PROP_KEY = "item_per_page";
9 |
10 | export function usePaginationControls(items?: T[]) {
11 | const storedItemsPerPage = Number(readProp("item_per_page", "10"));
12 | const [itemsPerPage, setItemsPerPage] = useState(
13 | storedItemsPerPage ? storedItemsPerPage : 10
14 | );
15 | const [page, setPage] = useState(0);
16 |
17 | return {
18 | itemsPerPage: itemsPerPage,
19 | setItemsPerPage: setItemsPerPage,
20 | page: page,
21 | setPage: setPage,
22 | items: items,
23 | pageItems: () => items?.slice(page * itemsPerPage, (page + 1) * itemsPerPage), // slice for pagination
24 | };
25 | }
26 |
27 | export function PaginatedContainer(
28 | props: PropsWithChildren<{
29 | controls: ReturnType;
30 | }>
31 | ) {
32 | const { controls, children } = props;
33 | const { items, itemsPerPage, page, setItemsPerPage, setPage } = controls;
34 |
35 | const { lang } = useLanguage();
36 |
37 | const locale = {
38 | en: enUS,
39 | es: esES,
40 | ru: ruRU,
41 | zh: zhCN,
42 | }[lang];
43 |
44 | const containerRef = useRef(null);
45 |
46 | return (
47 |
48 | {children}
49 |
50 | {items && (
51 | createTheme(outerTheme, locale)}>
52 | {
59 | containerRef?.current?.scrollIntoView();
60 | setPage(page);
61 | }}
62 | onChangeRowsPerPage={(event) => {
63 | setItemsPerPage(parseInt(event.target.value, 10));
64 | writeProp(PROP_KEY, event.target.value);
65 | setPage(0);
66 | }}
67 | />
68 |
69 | )}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/util/ResponsiveBox.tsx:
--------------------------------------------------------------------------------
1 | import React, { PropsWithChildren } from "react";
2 | import { Box, BoxProps, Theme, useMediaQuery } from "@material-ui/core";
3 | import { Breakpoint } from "@material-ui/core/styles/createBreakpoints";
4 |
5 | export function ResponsiveBox(
6 | props: PropsWithChildren<
7 | {
8 | breakpoint: Breakpoint;
9 | } & BoxProps
10 | >
11 | ) {
12 | const { breakpoint, children, ...boxProps } = props;
13 | const responsive = useMediaQuery((theme) => theme.breakpoints.down(breakpoint));
14 |
15 | return (
16 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/util/ResponsiveContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Container, createStyles, makeStyles } from "@material-ui/core";
2 | import { ReactNode } from "react";
3 |
4 | const useStyles = makeStyles(({ breakpoints }) =>
5 | createStyles({
6 | container: {
7 | // margin: 'auto',
8 | width: "100%",
9 | maxWidth: "100%",
10 | [breakpoints.up("sm")]: {
11 | maxWidth: breakpoints.values.sm,
12 | },
13 | [breakpoints.up("md")]: {
14 | maxWidth: breakpoints.values.md,
15 | },
16 | [breakpoints.up("lg")]: {
17 | maxWidth: breakpoints.values.lg,
18 | },
19 | },
20 | })
21 | );
22 |
23 | export function ResponsiveContainer(props: { children: ReactNode }) {
24 | const classes = useStyles();
25 | return {props.children}>} />;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/util/SadFace.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@material-ui/core";
2 | import { SentimentDissatisfied } from "@material-ui/icons";
3 | import { PropsWithChildren } from "react";
4 |
5 | export function SadFace(props: PropsWithChildren<{}>) {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/util/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties, useState } from "react";
2 | import { createStyles, Divider, IconButton, InputBase, makeStyles, Paper } from "@material-ui/core";
3 | import { ArrowForward, Clear, Search } from "@material-ui/icons";
4 |
5 | const useStyles = makeStyles(({ breakpoints, spacing }) =>
6 | createStyles({
7 | root: {
8 | padding: "2px 4px",
9 | display: "flex",
10 | alignItems: "center",
11 | background: "rgb(0,0,0,0.2)",
12 | boxShadow: "none",
13 | width: "100%",
14 | maxWidth: spacing(50),
15 | [breakpoints.down("xs")]: {
16 | maxWidth: "100%",
17 | },
18 | },
19 | input: {
20 | marginLeft: spacing(1),
21 | flex: 1,
22 | },
23 | iconButton: {
24 | padding: spacing(1),
25 | },
26 | divider: {
27 | height: 28,
28 | margin: 4,
29 | },
30 | })
31 | );
32 |
33 | export function SearchBar(props: {
34 | placeholder: string;
35 | onClear?: (query: "") => void;
36 | onSearch: (query: string) => void;
37 | style?: CSSProperties;
38 | }) {
39 | const { placeholder, onClear, onSearch, style } = props;
40 | const classes = useStyles();
41 | const [searchQuery, setSearchQuery] = useState("");
42 |
43 | return (
44 |
45 |
46 | setSearchQuery(event.target.value)}
50 | placeholder={placeholder}
51 | onKeyPress={(event) => {
52 | if (event.key === "Enter") {
53 | onSearch(searchQuery);
54 | event.preventDefault();
55 | }
56 | }}
57 | />
58 | {
62 | onClear?.("");
63 | setSearchQuery("");
64 | }}
65 | >
66 |
67 |
68 |
69 | onSearch(searchQuery)}>
70 |
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/view-items/GameCard.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Card } from "@material-ui/core";
2 | import { CSSProperties, useState } from "react";
3 | import { OverflowText } from "../util/OverflowText";
4 | import { Link } from "react-router-dom";
5 | import { path } from "../../util/paths";
6 | import { GameCardData } from "../../util/types";
7 |
8 | export function GameCard(props: { data: GameCardData; style?: CSSProperties }) {
9 | const { data, style } = props;
10 | const [raised, setRaised] = useState(false);
11 |
12 | return (
13 |
14 | setRaised(true)}
18 | onMouseLeave={() => setRaised(false)}
19 | >
20 |
21 |
28 |
29 |
30 |
31 |
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/view-items/OfferRow.tsx:
--------------------------------------------------------------------------------
1 | import { OfferRowData } from "../../util/types";
2 | import React, { useState } from "react";
3 | import {
4 | Box,
5 | Collapse,
6 | IconButton,
7 | Popover,
8 | Table,
9 | TableBody,
10 | TableCell,
11 | TableRow,
12 | } from "@material-ui/core";
13 | import { KeyboardArrowDown, KeyboardArrowUp, Panorama } from "@material-ui/icons";
14 | import { useLocale } from "../../hooks/locale";
15 | import { bindTrigger, usePopupState } from "material-ui-popup-state/hooks";
16 | import { bindPopover } from "material-ui-popup-state";
17 |
18 | export function OfferRow(props: { data: OfferRowData }) {
19 | const { data } = props;
20 | const [open, setOpen] = useState(false);
21 | const { locale } = useLocale();
22 |
23 | const popupState = usePopupState({
24 | variant: "popover",
25 | popupId: "offer-img",
26 | });
27 |
28 | return (
29 | <>
30 |
31 |
32 | {data.items.length > 1 && (
33 | setOpen(!open)}>
34 | {open ? : }
35 |
36 | )}
37 |
38 |
39 |
40 | {data.image ? (
41 |
49 | ) : (
50 |
51 | )}
52 |
53 |
65 |
66 |
75 |
76 |
77 |
78 | {data.title}
79 | {data.items.length === 1 ? data.items[0].id : locale.multiple_items}
80 | {data.offerType}
81 |
82 | {data.items.length > 1 && (
83 |
84 |
85 |
86 |
87 |
88 | {data.items.map((item) => (
89 |
90 |
91 | {item.title}
92 | {item.id}
93 |
94 | ))}
95 |
96 |
97 |
98 |
99 |
100 | )}
101 | >
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/views/TableView.tsx:
--------------------------------------------------------------------------------
1 | import { OfferRowData } from "../../util/types";
2 | import {
3 | Box,
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableContainer,
8 | TableHead,
9 | TableRow,
10 | } from "@material-ui/core";
11 | import { useLocale } from "../../hooks/locale";
12 | import { OfferRowSkeleton } from "../skeletons/OfferRowSkeleton";
13 | import { usePaginationControls } from "../util/PaginatedContainer";
14 | import React from "react";
15 | import { SearchBar } from "../util/SearchBar";
16 | import { OfferRow } from "../view-items/OfferRow";
17 | import { OfferType } from "../../generated/graphql";
18 | import { OfferTypeFilter } from "../offers/OfferTypeFilter";
19 | import { ResponsiveBox } from "../util/ResponsiveBox";
20 |
21 | const usePaginationControlsWrapper = () => usePaginationControls();
22 |
23 | export function TableView(props: {
24 | pagination: ReturnType;
25 | setFilterID: (idFilter: string) => void;
26 | offerTypeFilters: Record;
27 | setOfferTypeFilters: (typeFilters: Record) => void;
28 | }) {
29 | const { pagination, setFilterID, offerTypeFilters, setOfferTypeFilters } = props;
30 | const { locale } = useLocale();
31 |
32 | function onSearch(query: string) {
33 | setFilterID(query);
34 | pagination.setPage(0);
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {locale.image}
54 | {locale.title}
55 | {locale.id}
56 | {locale.offer_type}
57 |
58 |
59 |
60 | {pagination.pageItems()?.map((item) => ) ??
61 | [...Array(pagination.itemsPerPage).keys()].map((it) => )}
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/context/ContextProviders.tsx:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren, useState } from "react";
2 | import { LanguageContext } from "./language";
3 | import { KeywordsContext } from "./keywords";
4 | import { readProp, writeProp } from "../util/storage";
5 | import { ValidLanguage } from "../util/types";
6 |
7 | export function ContextProviders(props: PropsWithChildren<{}>) {
8 | let storedLang = readProp("lang", "en");
9 | if (!["en", "es", "ru", "zh"].includes(storedLang)) {
10 | storedLang = "en";
11 | writeProp("lang", "en");
12 | }
13 |
14 | const [language, setLanguage] = useState(storedLang as ValidLanguage);
15 | const langValue = {
16 | lang: language,
17 | setLang: (key: ValidLanguage) => {
18 | setLanguage(key);
19 | writeProp("lang", key);
20 | },
21 | };
22 |
23 | const [keywords, setKeywords] = useState("");
24 | const keywordsValue = {
25 | keywords: keywords,
26 | setKeywords: setKeywords,
27 | };
28 |
29 | return (
30 |
31 | {props.children}
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/context/keywords.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 |
3 | export const KeywordsContext = createContext<{
4 | keywords: string;
5 | setKeywords: (key: string) => void;
6 | }>({
7 | keywords: "",
8 | setKeywords: () => {},
9 | });
10 | export const useKeywords = () => useContext(KeywordsContext);
11 |
--------------------------------------------------------------------------------
/src/context/language.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from "react";
2 | import { ValidLanguage } from "../util/types";
3 |
4 | export const LanguageContext = createContext<{
5 | lang: ValidLanguage;
6 | setLang: (key: ValidLanguage) => void;
7 | }>({
8 | lang: "en",
9 | setLang: () => {},
10 | });
11 | export const useLanguage = () => useContext(LanguageContext);
12 |
--------------------------------------------------------------------------------
/src/generated/graphql.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLResolveInfo } from "graphql";
2 | import { GraphQLClient } from "graphql-request";
3 | import * as Dom from "graphql-request/dist/types.dom";
4 | import gql from "graphql-tag";
5 |
6 | export type Maybe = T | null;
7 | export type Exact = { [K in keyof T]: T[K] };
8 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
9 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
10 | export type RequireFields =
11 | { [X in Exclude]?: T[X] }
12 | & { [P in K]-?: NonNullable };
13 | /** All built-in and custom scalars, mapped to their actual values */
14 | export type Scalars = {
15 | ID: string;
16 | String: string;
17 | Boolean: boolean;
18 | Int: number;
19 | Float: number;
20 | };
21 |
22 | export type CatalogNs = {
23 | __typename?: "CatalogNs";
24 | mappings: Array;
25 | };
26 |
27 |
28 | export type CatalogNsMappingsArgs = {
29 | pageType?: Maybe;
30 | };
31 |
32 | export type CatalogOffersParams = {
33 | count?: Maybe;
34 | };
35 |
36 | export type CatalogQuery = {
37 | __typename?: "CatalogQuery";
38 | searchStore: Elements;
39 | catalogOffers: Elements;
40 | };
41 |
42 |
43 | export type CatalogQuerySearchStoreArgs = {
44 | category?: Maybe;
45 | count?: Maybe;
46 | namespace?: Maybe;
47 | keywords?: Maybe;
48 | sortBy?: Maybe;
49 | sortDir?: Maybe;
50 | };
51 |
52 |
53 | export type CatalogQueryCatalogOffersArgs = {
54 | namespace: Scalars["String"];
55 | params?: Maybe;
56 | };
57 |
58 | export type Element = {
59 | __typename?: "Element";
60 | id: Scalars["String"];
61 | title: Scalars["String"];
62 | namespace: Scalars["String"];
63 | offerType: OfferType;
64 | items: Array- ;
65 | keyImages: Array;
66 | creationDate: Scalars["String"];
67 | catalogNs: CatalogNs;
68 | };
69 |
70 | export type Elements = {
71 | __typename?: "Elements";
72 | elements: Array;
73 | };
74 |
75 | export type Item = {
76 | __typename?: "Item";
77 | id: Scalars["String"];
78 | title: Scalars["String"];
79 | namespace: Scalars["String"];
80 | };
81 |
82 | export type KeyImage = {
83 | __typename?: "KeyImage";
84 | type: KeyImageType;
85 | url?: Maybe;
86 | };
87 |
88 | export enum KeyImageType {
89 | OfferImageTall = "OfferImageTall",
90 | OfferImageWide = "OfferImageWide",
91 | Thumbnail = "Thumbnail",
92 | CodeRedemption_340x440 = "CodeRedemption_340x440",
93 | DieselStoreFrontTall = "DieselStoreFrontTall",
94 | DieselStoreFrontWide = "DieselStoreFrontWide"
95 | }
96 |
97 | export type Mapping = {
98 | __typename?: "Mapping";
99 | pageSlug: Scalars["String"];
100 | };
101 |
102 | export enum OfferType {
103 | AddOn = "ADD_ON",
104 | BaseGame = "BASE_GAME",
105 | Bundle = "BUNDLE",
106 | Edition = "EDITION",
107 | Dlc = "DLC",
108 | Others = "OTHERS",
109 | Unlockable = "UNLOCKABLE"
110 | }
111 |
112 | export type Query = {
113 | __typename?: "Query";
114 | Catalog?: Maybe;
115 | };
116 |
117 | export type SearchGamesQueryVariables = Exact<{
118 | keywords: Scalars["String"];
119 | sortBy?: Maybe;
120 | sortDir?: Maybe;
121 | }>;
122 |
123 |
124 | export type SearchGamesQuery = (
125 | { __typename?: "Query" }
126 | & {
127 | Catalog?: Maybe<(
128 | { __typename?: "CatalogQuery" }
129 | & {
130 | searchStore: (
131 | { __typename?: "Elements" }
132 | & {
133 | elements: Array<(
134 | { __typename?: "Element" }
135 | & Pick
136 | & {
137 | items: Array<(
138 | { __typename?: "Item" }
139 | & Pick
-
140 | )>, keyImages: Array<(
141 | { __typename?: "KeyImage" }
142 | & Pick
143 | )>
144 | }
145 | )>
146 | }
147 | )
148 | }
149 | )>
150 | }
151 | );
152 |
153 | export type SearchOffersQueryVariables = Exact<{
154 | namespace: Scalars["String"];
155 | }>;
156 |
157 |
158 | export type SearchOffersQuery = (
159 | { __typename?: "Query" }
160 | & {
161 | Catalog?: Maybe<(
162 | { __typename?: "CatalogQuery" }
163 | & {
164 | catalogOffers: (
165 | { __typename?: "Elements" }
166 | & {
167 | elements: Array<(
168 | { __typename?: "Element" }
169 | & Pick
170 | & {
171 | items: Array<(
172 | { __typename?: "Item" }
173 | & Pick
-
174 | )>, keyImages: Array<(
175 | { __typename?: "KeyImage" }
176 | & Pick
177 | )>
178 | }
179 | )>
180 | }
181 | ), searchStore: (
182 | { __typename?: "Elements" }
183 | & {
184 | elements: Array<(
185 | { __typename?: "Element" }
186 | & Pick
187 | & {
188 | catalogNs: (
189 | { __typename?: "CatalogNs" }
190 | & {
191 | mappings: Array<(
192 | { __typename?: "Mapping" }
193 | & Pick
194 | )>
195 | }
196 | )
197 | }
198 | )>
199 | }
200 | )
201 | }
202 | )>
203 | }
204 | );
205 |
206 |
207 | export type ResolverTypeWrapper = Promise | T;
208 |
209 |
210 | export type ResolverWithResolve = {
211 | resolve: ResolverFn;
212 | };
213 |
214 | export type LegacyStitchingResolver = {
215 | fragment: string;
216 | resolve: ResolverFn;
217 | };
218 |
219 | export type NewStitchingResolver = {
220 | selectionSet: string;
221 | resolve: ResolverFn;
222 | };
223 | export type StitchingResolver =
224 | LegacyStitchingResolver
225 | | NewStitchingResolver;
226 | export type Resolver =
227 | | ResolverFn
228 | | ResolverWithResolve
229 | | StitchingResolver;
230 |
231 | export type ResolverFn = (
232 | parent: TParent,
233 | args: TArgs,
234 | context: TContext,
235 | info: GraphQLResolveInfo
236 | ) => Promise | TResult;
237 |
238 | export type SubscriptionSubscribeFn = (
239 | parent: TParent,
240 | args: TArgs,
241 | context: TContext,
242 | info: GraphQLResolveInfo
243 | ) => AsyncIterator | Promise>;
244 |
245 | export type SubscriptionResolveFn = (
246 | parent: TParent,
247 | args: TArgs,
248 | context: TContext,
249 | info: GraphQLResolveInfo
250 | ) => TResult | Promise;
251 |
252 | export interface SubscriptionSubscriberObject {
253 | subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>;
254 | resolve?: SubscriptionResolveFn;
255 | }
256 |
257 | export interface SubscriptionResolverObject {
258 | subscribe: SubscriptionSubscribeFn;
259 | resolve: SubscriptionResolveFn;
260 | }
261 |
262 | export type SubscriptionObject =
263 | | SubscriptionSubscriberObject
264 | | SubscriptionResolverObject;
265 |
266 | export type SubscriptionResolver =
267 | | ((...args: any[]) => SubscriptionObject)
268 | | SubscriptionObject;
269 |
270 | export type TypeResolveFn = (
271 | parent: TParent,
272 | context: TContext,
273 | info: GraphQLResolveInfo
274 | ) => Maybe | Promise>;
275 |
276 | export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise;
277 |
278 | export type NextResolverFn = () => Promise;
279 |
280 | export type DirectiveResolverFn = (
281 | next: NextResolverFn,
282 | parent: TParent,
283 | args: TArgs,
284 | context: TContext,
285 | info: GraphQLResolveInfo
286 | ) => TResult | Promise;
287 |
288 | /** Mapping between all available schema types and the resolvers types */
289 | export type ResolversTypes = {
290 | CatalogNs: ResolverTypeWrapper;
291 | String: ResolverTypeWrapper;
292 | CatalogOffersParams: CatalogOffersParams;
293 | Int: ResolverTypeWrapper;
294 | CatalogQuery: ResolverTypeWrapper;
295 | Element: ResolverTypeWrapper;
296 | Elements: ResolverTypeWrapper;
297 | Item: ResolverTypeWrapper
- ;
298 | KeyImage: ResolverTypeWrapper;
299 | KeyImageType: KeyImageType;
300 | Mapping: ResolverTypeWrapper;
301 | OfferType: OfferType;
302 | Query: ResolverTypeWrapper<{}>;
303 | Boolean: ResolverTypeWrapper;
304 | };
305 |
306 | /** Mapping between all available schema types and the resolvers parents */
307 | export type ResolversParentTypes = {
308 | CatalogNs: CatalogNs;
309 | String: Scalars["String"];
310 | CatalogOffersParams: CatalogOffersParams;
311 | Int: Scalars["Int"];
312 | CatalogQuery: CatalogQuery;
313 | Element: Element;
314 | Elements: Elements;
315 | Item: Item;
316 | KeyImage: KeyImage;
317 | Mapping: Mapping;
318 | Query: {};
319 | Boolean: Scalars["Boolean"];
320 | };
321 |
322 | export type CatalogNsResolvers = {
323 | mappings?: Resolver, ParentType, ContextType, RequireFields>;
324 | __isTypeOf?: IsTypeOfResolverFn;
325 | };
326 |
327 | export type CatalogQueryResolvers = {
328 | searchStore?: Resolver>;
329 | catalogOffers?: Resolver>;
330 | __isTypeOf?: IsTypeOfResolverFn;
331 | };
332 |
333 | export type ElementResolvers = {
334 | id?: Resolver;
335 | title?: Resolver;
336 | namespace?: Resolver;
337 | offerType?: Resolver;
338 | items?: Resolver, ParentType, ContextType>;
339 | keyImages?: Resolver, ParentType, ContextType>;
340 | creationDate?: Resolver;
341 | catalogNs?: Resolver;
342 | __isTypeOf?: IsTypeOfResolverFn;
343 | };
344 |
345 | export type ElementsResolvers = {
346 | elements?: Resolver, ParentType, ContextType>;
347 | __isTypeOf?: IsTypeOfResolverFn;
348 | };
349 |
350 | export type ItemResolvers = {
351 | id?: Resolver;
352 | title?: Resolver;
353 | namespace?: Resolver;
354 | __isTypeOf?: IsTypeOfResolverFn;
355 | };
356 |
357 | export type KeyImageResolvers = {
358 | type?: Resolver;
359 | url?: Resolver, ParentType, ContextType>;
360 | __isTypeOf?: IsTypeOfResolverFn;
361 | };
362 |
363 | export type MappingResolvers = {
364 | pageSlug?: Resolver;
365 | __isTypeOf?: IsTypeOfResolverFn;
366 | };
367 |
368 | export type QueryResolvers = {
369 | Catalog?: Resolver, ParentType, ContextType>;
370 | };
371 |
372 | export type Resolvers = {
373 | CatalogNs?: CatalogNsResolvers;
374 | CatalogQuery?: CatalogQueryResolvers;
375 | Element?: ElementResolvers;
376 | Elements?: ElementsResolvers;
377 | Item?: ItemResolvers;
378 | KeyImage?: KeyImageResolvers;
379 | Mapping?: MappingResolvers;
380 | Query?: QueryResolvers;
381 | };
382 |
383 |
384 | /**
385 | * @deprecated
386 | * Use "Resolvers" root object instead. If you wish to get "IResolvers", add "typesPrefix: I" to your config.
387 | */
388 | export type IResolvers = Resolvers;
389 |
390 |
391 | export const SearchGamesDocument = gql`
392 | query searchGames($keywords: String!, $sortBy: String, $sortDir: String) {
393 | Catalog {
394 | searchStore(
395 | category: "games/edition/base"
396 | count: 1000
397 | keywords: $keywords
398 | sortBy: $sortBy
399 | sortDir: $sortDir
400 | ) {
401 | elements {
402 | id
403 | title
404 | namespace
405 | creationDate
406 | items {
407 | id
408 | namespace
409 | }
410 | keyImages {
411 | type
412 | url
413 | }
414 | }
415 | }
416 | }
417 | }
418 | `;
419 | export const SearchOffersDocument = gql`
420 | query searchOffers($namespace: String!) {
421 | Catalog {
422 | catalogOffers(namespace: $namespace, params: {count: 1000}) {
423 | elements {
424 | id
425 | title
426 | offerType
427 | items {
428 | id
429 | }
430 | keyImages {
431 | type
432 | url
433 | }
434 | }
435 | }
436 | searchStore(category: "games/edition/base", namespace: $namespace) {
437 | elements {
438 | title
439 | catalogNs {
440 | mappings(pageType: "productHome") {
441 | pageSlug
442 | }
443 | }
444 | }
445 | }
446 | }
447 | }
448 | `;
449 |
450 | export type SdkFunctionWrapper = (action: (requestHeaders?: Record) => Promise, operationName: string) => Promise;
451 |
452 |
453 | const defaultWrapper: SdkFunctionWrapper = (action, _operationName) => action();
454 |
455 | export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
456 | return {
457 | searchGames(variables: SearchGamesQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise {
458 | return withWrapper((wrappedRequestHeaders) => client.request(SearchGamesDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "searchGames");
459 | },
460 | searchOffers(variables: SearchOffersQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise {
461 | return withWrapper((wrappedRequestHeaders) => client.request(SearchOffersDocument, variables, { ...requestHeaders, ...wrappedRequestHeaders }), "searchOffers");
462 | }
463 | };
464 | }
465 |
466 | export type Sdk = ReturnType;
--------------------------------------------------------------------------------
/src/hooks/locale.ts:
--------------------------------------------------------------------------------
1 | import { useLanguage } from "../context/language";
2 | import { rawLocale } from "../util/locale";
3 |
4 | export function useLocale() {
5 | const { lang } = useLanguage();
6 |
7 | return {
8 | locale: new Proxy>(
9 | // @ts-ignore
10 | rawLocale,
11 | {
12 | get(target, name, receiver) {
13 | let rv = Reflect.get(target, name, receiver);
14 | if (name in target) rv = rv[lang] ?? rv["en"];
15 | return rv;
16 | },
17 | set(): boolean {
18 | return false;
19 | },
20 | }
21 | ),
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/screen-size.ts:
--------------------------------------------------------------------------------
1 | import { Theme, useMediaQuery } from "@material-ui/core";
2 |
3 | export const useXS = () => useMediaQuery((theme) => theme.breakpoints.down("xs"));
4 | export const useSM = () => useMediaQuery((theme) => theme.breakpoints.down("sm"));
5 | export const useMD = () => useMediaQuery((theme) => theme.breakpoints.down("md"));
6 | export const useLG = () => useMediaQuery((theme) => theme.breakpoints.down("lg"));
7 | export const usePortrait = () => useMediaQuery("(orientation: portrait)");
8 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | html {
16 | background-color: #303030;
17 | }
18 |
19 | @media only screen and (min-width: 600px) {
20 | html, body {
21 | overflow: hidden;
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./index.css";
4 | import App from "./App";
5 | import reportWebVitals from "./reportWebVitals";
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById("root")
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/md/home_en.md:
--------------------------------------------------------------------------------
1 | # 🐨 Welcome to the ScreamDB!
2 |
3 | ___
4 |
5 | ## 🙋🏻♀️ Questions and answers
6 |
7 | * ### What is ScreamDB?
8 | ScreamDB is a web application for viewing DLC IDs of all games from the Epic Games store.
9 |
10 | * ### What is the use for DLC IDs?
11 | DLC IDs can be used for enabling/disabling desired DLCs in the **[ScreamAPI]** config file.
12 |
13 | * ### Where is the data coming from?
14 | The data comes directly from the official Epic Games servers, so it is always up-to-date.
15 |
16 | ___
17 |
18 | ## 👩🏻💻 Extra info for developers
19 |
20 | * The web app is open-source and available at [GitHub].
21 |
22 | [ScreamAPI]: https://cs.rin.ru/forum/viewtopic.php?f=29&t=106474
23 |
24 | [GraphQL endpoint]: https://www.epicgames.com/graphql
25 |
26 | [GitHub]: https://github.com/acidicoala/ScreamDB
27 |
--------------------------------------------------------------------------------
/src/md/home_es.md:
--------------------------------------------------------------------------------
1 | # 🐨 ¡Bienvenido a ScreamDB!
2 |
3 |
4 |
5 | ___
6 |
7 |
8 |
9 | ## 🙋🏻♀️ Preguntas y respuestas
10 |
11 |
12 |
13 | * ### ¿Qué es ScreamDB?
14 |
15 | ScreamDB es una aplicación web para ver las IDs de los DLCs de todos los juegos de la tienda de Epic Games.
16 |
17 | * ### ¿Para qué sirven las IDs de los DLCs?
18 |
19 | Las IDs de DLCs pueden ser usadas para activar/desactivar ciertos DLCs en el archivo de configuración de **[ScreamAPI]**.
20 |
21 | * ### ¿De dónde proviene esta información?
22 |
23 | La información proviene directamente de los servidores oficiales de Epic Games, así que siempre está actualizada.
24 | ___
25 |
26 | ## 👩🏻💻 Información adicional para desarrolladores
27 |
28 | * La aplicación web es de código abierto y está disponible en [GitHub].
29 |
30 | [ScreamAPI]: https://cs.rin.ru/forum/viewtopic.php?f=29&t=106474
31 |
32 | [GraphQL endpoint]: https://www.epicgames.com/graphql
33 |
34 | [GitHub]: https://github.com/acidicoala/ScreamDB
35 |
--------------------------------------------------------------------------------
/src/md/home_ru.md:
--------------------------------------------------------------------------------
1 | # 🐨 Добро пожаловать на ScreamDB!
2 |
3 | ___
4 |
5 | ## 🙋🏻♀️ Вопросы и ответы
6 |
7 | * ### Что такое ScreamDB?
8 | ScreamDB это веб приложение для просмотра идентификаторов (ID) дополнительного контента (DLC) для игр доступных в магазине Epic Games.
9 |
10 | * ### Зачем нужны DLC ID?
11 | DLC ID могут быть использованы для включения/отключения желаемых DLC в конфигурационном файле **[ScreamAPI]**.
12 |
13 | * ### Откуда берутся данные?
14 | Данные берутся напрямую с официальнов сереверов Epic Games, что делает их всегда актуальными.
15 |
16 | ___
17 |
18 | ## 👩🏻💻 Дополнительная информация для разработчиков
19 |
20 | * Это веб приложение с открытым исходным кодом доступно на [GitHub].
21 |
22 | [ScreamAPI]: https://cs.rin.ru/forum/viewtopic.php?f=29&t=106474
23 |
24 | [конечной точки GraphQL]: https://www.epicgames.com/graphql
25 |
26 | [GitHub]: https://github.com/acidicoala/ScreamDB
27 |
--------------------------------------------------------------------------------
/src/md/home_zh.md:
--------------------------------------------------------------------------------
1 | # 🐨 欢迎来到 ScreamDB!
2 |
3 | ___
4 |
5 | ## 🙋🏻♀️ 常见问题
6 |
7 | * ### ScreamDB 是什么?
8 | ScreamDB是一个用于查找Epic商城中的游戏DLC ID的网站。
9 |
10 | * ### DLC ID 能做什么?
11 | DLC ID可以用于在 **[ScreamAPI]** 的配置文件中启用或禁用DLC。
12 |
13 | * ### 数据来源是?
14 | 这些数据直接来自Epic官方服务器,所以永远是最新的。
15 |
16 | ___
17 |
18 | ## 👩🏻💻 给开发者的额外信息
19 |
20 | * 本网站在 [GitHub] 上开源。
21 |
22 | [ScreamAPI]: https://cs.rin.ru/forum/viewtopic.php?f=29&t=106474
23 |
24 | [GraphQL endpoint]: https://www.epicgames.com/graphql
25 |
26 | [GitHub]: https://github.com/acidicoala/ScreamDB
27 |
--------------------------------------------------------------------------------
/src/pages/Games.tsx:
--------------------------------------------------------------------------------
1 | import { Box, createStyles, makeStyles, Typography, useTheme } from "@material-ui/core";
2 | import React, { useEffect, useState } from "react";
3 | import { GameCardData, ValidSortDirection } from "../util/types";
4 | import { useKeywords } from "../context/keywords";
5 | import { sdk } from "../util/query";
6 | import { SadFace } from "../components/util/SadFace";
7 | import { useLocale } from "../hooks/locale";
8 | import { Log } from "../util/log";
9 | import { PaginatedContainer, usePaginationControls } from "../components/util/PaginatedContainer";
10 | import { GameCard } from "../components/view-items/GameCard";
11 | import { GameCardSkeleton } from "../components/skeletons/GameCardSkeleton";
12 | import { KeyImageType } from "../generated/graphql";
13 | import { SortBySelect, SortOption } from "../components/games/SortBySelect";
14 | import { Sort } from "@material-ui/icons";
15 | import { SortDirButton } from "../components/games/SortDirButton";
16 | import { readProp } from "../util/storage";
17 | import { ResponsiveBox } from "../components/util/ResponsiveBox";
18 | import { Skeleton } from "@material-ui/lab";
19 |
20 | const useStyles = makeStyles(({ breakpoints }) =>
21 | createStyles({
22 | grid: {
23 | display: "flex",
24 | margin: "auto",
25 | flexWrap: "wrap",
26 | justifyContent: "center",
27 | width: "100%",
28 | maxWidth: "100%",
29 | [breakpoints.up("sm")]: {
30 | maxWidth: breakpoints.values.sm,
31 | },
32 | [breakpoints.up("md")]: {
33 | maxWidth: breakpoints.values.md,
34 | },
35 | [breakpoints.up("lg")]: {
36 | maxWidth: breakpoints.values.lg,
37 | },
38 | },
39 | })
40 | );
41 |
42 | export function Games() {
43 | const { spacing } = useTheme();
44 | const classes = useStyles();
45 | const { locale } = useLocale();
46 | const { keywords } = useKeywords();
47 | const [games, setGames] = useState();
48 | const pagination = usePaginationControls(games);
49 |
50 | const [sortBy, setSortBy] = useState(
51 | readProp("sort_games_by", SortOption.RELEVANCE) as SortOption
52 | );
53 | const [sortDir, setSortDir] = useState(
54 | readProp("sort_games_dir", "ASC") as ValidSortDirection
55 | );
56 |
57 | useEffect(() => {
58 | setGames(undefined);
59 | pagination.setPage(0);
60 |
61 | sdk
62 | .searchGames({ keywords: keywords, sortBy: sortBy, sortDir: sortDir })
63 | .then((it) => it.Catalog!.searchStore.elements)
64 | .then((elements) => {
65 | setGames(
66 | elements.map((element) => ({
67 | id: element.id,
68 | title: element.title,
69 | namespace: element.namespace,
70 | image: element.keyImages?.find((image) => image.type === KeyImageType.OfferImageTall)
71 | ?.url,
72 | creationDate: new Date(element.creationDate),
73 | }))
74 | );
75 | })
76 | .catch((reason) => {
77 | Log.error(reason);
78 | setGames([]);
79 | });
80 | }, [keywords, sortBy, sortDir]);
81 |
82 | return (
83 |
84 | {" "}
85 | {games?.length === 0 ? (
86 |
87 | ) : (
88 |
89 |
90 | {games ? (
91 |
92 | ) : (
93 |
94 | )}
95 |
96 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | {pagination
118 | .pageItems()
119 | ?.map((it) => ) ??
120 | [...Array(pagination.itemsPerPage).keys()].map((it) => (
121 |
122 | ))}
123 |
124 |
125 | )}
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Box, createStyles, makeStyles } from "@material-ui/core";
2 |
3 | import ReactMarkdown from "react-markdown";
4 | import React, { useEffect, useState } from "react";
5 | import home_en from "../md/home_en.md";
6 | import home_es from "../md/home_es.md";
7 | import home_ru from "../md/home_ru.md";
8 | import home_zh from "../md/home_zh.md";
9 | import { useLanguage } from "../context/language";
10 |
11 | const useStyles = makeStyles(() =>
12 | createStyles({
13 | home: {
14 | "& a": {
15 | color: "#0E0",
16 | textDecoration: "none",
17 | "&:hover": {
18 | textDecoration: "underline",
19 | },
20 | },
21 | },
22 | })
23 | );
24 |
25 | export function Home() {
26 | const { lang } = useLanguage();
27 | const classes = useStyles();
28 |
29 | const [home, setHome] = useState({
30 | en: "",
31 | es: "",
32 | ru: "",
33 | zh: "",
34 | });
35 |
36 | useEffect(() => {
37 | const en = fetch(home_en).then((file) => file.text());
38 | const es = fetch(home_es).then((file) => file.text());
39 | const ru = fetch(home_ru).then((file) => file.text());
40 | const zh = fetch(home_zh).then((file) => file.text());
41 |
42 | Promise.all([en, es, ru, zh]).then(([en, es, ru, zh]) =>
43 | setHome({
44 | en: en,
45 | es: es,
46 | ru: ru,
47 | zh: zh,
48 | })
49 | );
50 | }, []);
51 |
52 | return (
53 |
54 |
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/Offers.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { Box, IconButton, Tooltip, Typography } from "@material-ui/core";
4 | import { Skeleton } from "@material-ui/lab";
5 | import { Launch } from "@material-ui/icons";
6 | import { SadFace } from "../components/util/SadFace";
7 | import { OfferRowData } from "../util/types";
8 | import { useLocale } from "../hooks/locale";
9 | import { sdk } from "../util/query";
10 | import { Log } from "../util/log";
11 | import { TableView } from "../components/views/TableView";
12 | import { PaginatedContainer, usePaginationControls } from "../components/util/PaginatedContainer";
13 | import { readProp, writeProp } from "../util/storage";
14 | import { Element, OfferType } from "../generated/graphql";
15 |
16 | export function Offers() {
17 | const { locale } = useLocale();
18 | const { namespace } = useParams<{ namespace?: string }>();
19 |
20 | const [filterID, setFilterID] = useState("");
21 | const [offers, setOffers] = useState();
22 | const [gameInfo, setGameInfo] = useState>();
23 |
24 | let initialOfferTypesFilters: Record = {
25 | ADD_ON: true,
26 | BASE_GAME: true,
27 | BUNDLE: true,
28 | EDITION: true,
29 | DLC: true,
30 | OTHERS: true,
31 | UNLOCKABLE: true,
32 | };
33 |
34 | try {
35 | const storedOfferTypeFilters = JSON.parse(readProp("type_filters", "{}"));
36 | initialOfferTypesFilters = {
37 | ...initialOfferTypesFilters,
38 | ...storedOfferTypeFilters,
39 | };
40 | } catch (e) {
41 | Log.error(e);
42 | }
43 |
44 | const [offerTypeFilters, setOfferTypeFilters] =
45 | useState>(initialOfferTypesFilters);
46 |
47 | const filteredOffers = offers
48 | ?.filter((it) => !filterID || it?.items?.some((item) => item.id.includes(filterID)))
49 | ?.filter((it) => offerTypeFilters[it.offerType] === true)
50 | ?.sort((first, second) => first.items.length - second.items.length);
51 |
52 | const pagination = usePaginationControls(filteredOffers);
53 |
54 | useEffect(() => {
55 | setOffers(undefined);
56 |
57 | if (!namespace) {
58 | setOffers([]);
59 | return;
60 | }
61 |
62 | sdk
63 | .searchOffers({ namespace: namespace })
64 | .then((it) => it.Catalog!)
65 | .then((catalog) => {
66 | const gameInfo = catalog.searchStore.elements[0];
67 | const elements = catalog.catalogOffers.elements;
68 |
69 | setGameInfo(gameInfo);
70 | setOffers(
71 | elements.map((element) => {
72 | const findImage = (type: string) =>
73 | element.keyImages?.find((image) => image.type === type)?.url;
74 |
75 | return {
76 | id: element.id,
77 | title: element.title,
78 | offerType: element.offerType,
79 | items: element.items.map((item) => ({
80 | title: elements.find((it) => it.items.length === 1 && it.items[0].id === item.id)
81 | ?.title,
82 | id: item.id,
83 | })),
84 | // Images are not guaranteed to be present, so try to pick the most reasonable
85 | image:
86 | findImage("OfferImageWide") ??
87 | findImage("DieselStoreFrontWide") ??
88 | findImage("Thumbnail") ??
89 | findImage("CodeRedemption_340x440") ??
90 | findImage("DieselStoreFrontTall") ??
91 | findImage("OfferImageTall") ??
92 | undefined,
93 | } as OfferRowData;
94 | })
95 | );
96 | })
97 | .catch((reason) => {
98 | Log.error(reason);
99 | setOffers([]);
100 | });
101 | }, [namespace]);
102 |
103 | return (
104 |
105 | {offers?.length === 0 ? (
106 |
107 | ) : (
108 |
109 |
110 |
111 | {gameInfo ? (
112 | <>
113 |
118 |
119 |
127 |
131 | }
132 | >
133 | } />
134 |
135 |
136 | >
137 | ) : (
138 |
139 | )}
140 |
141 |
142 |
143 | {
148 | setOfferTypeFilters(filters);
149 | writeProp("type_filters", JSON.stringify(filters));
150 | }}
151 | />
152 |
153 | )}
154 |
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals";
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/util/locale.ts:
--------------------------------------------------------------------------------
1 | export const rawLocale = {
2 | ascending: {
3 | en: "Ascending",
4 | ru: "По возрастающей",
5 | es: "Ascendente",
6 | zh: "升序",
7 | },
8 | browse: {
9 | en: "Browse",
10 | ru: "Обзор",
11 | es: "Navegar",
12 | zh: "浏览",
13 | },
14 | code_desc: {
15 | en: "You can directly copy and paste this code snippet at the bottom of the ScreamAPI config file",
16 | ru: "Вы можете напрямую скопировать и вставить данный фрагмент кода в конец конфигуграционного файла ScreamAPI",
17 | es: "Puedes copiar y pegar este retazo de código directamente al fondo del archivo de configuración de ScreamAPI",
18 | zh: "您可以直接将以下代码复制并粘贴至 ScreamAPI 配置文件底部",
19 | },
20 | copy: {
21 | en: "Copy",
22 | ru: "Скопировать",
23 | es: "Copiar",
24 | zh: "复制",
25 | },
26 | browse_games: {
27 | en: "Browse Games",
28 | ru: "Обзор Игр",
29 | es: "Navega juegos",
30 | zh: "浏览游戏",
31 | },
32 | clear_all: {
33 | en: "Clear all",
34 | ru: "Убрать все",
35 | es: "Limpiar todo",
36 | zh: "清空",
37 | },
38 | descending: {
39 | en: "Descending",
40 | ru: "По убывающей",
41 | es: "Descendiente",
42 | zh: "降序",
43 | },
44 | filter_by_offer_type: {
45 | en: "Filter by offer type",
46 | ru: "Фильтр по типу контента",
47 | es: "Filtrar por tipo de oferta",
48 | zh: "按类型过滤",
49 | },
50 | found_games: {
51 | en: "Found games",
52 | ru: "Найдено игр",
53 | es: "Juegos encontrados",
54 | zh: "已找到游戏",
55 | },
56 | home: {
57 | en: "Home",
58 | ru: "Домой",
59 | es: "Hogar",
60 | zh: "主页",
61 | },
62 | id: {
63 | en: "ID",
64 | ru: "ID",
65 | es: "ID",
66 | zh: "ID",
67 | },
68 | image: {
69 | en: "Image",
70 | ru: "Изображение",
71 | es: "Imagen",
72 | zh: "图像",
73 | },
74 | lang: {
75 | en: "English",
76 | ru: "Русский",
77 | es: "Español",
78 | zh: "中文(简体)",
79 | },
80 | multiple_items: {
81 | en: "Multiple items",
82 | ru: "Несколько предметов",
83 | es: "Múltiples elementos",
84 | zh: "多项",
85 | },
86 | no_offers: {
87 | en: "No offers found",
88 | ru: "Не найдено дополнительного контента",
89 | es: "No se han encontrado ofertas",
90 | zh: "未找到附加类容",
91 | },
92 | no_games: {
93 | en: "No games found",
94 | ru: "Не найдено игр",
95 | es: "No se han encontrado juegos",
96 | zh: "未找到游戏",
97 | },
98 | not_found: {
99 | en: "Page not found",
100 | ru: "Страница не найдена",
101 | es: "Página no encontrada",
102 | zh: "未找到页面",
103 | },
104 | offer_type: {
105 | en: "Offer type",
106 | ru: "Тип контента",
107 | es: "Tipo de oferta",
108 | zh: "类型",
109 | },
110 | search_by_id: {
111 | en: "Search by ID",
112 | ru: "Поиск по ID",
113 | es: "Buscar por ID",
114 | zh: "按 ID 搜索",
115 | },
116 | search_games: {
117 | en: "Search for games",
118 | ru: "Поиск игр",
119 | es: "Busca juegos",
120 | zh: "搜索游戏",
121 | },
122 | select_all: {
123 | en: "Select all",
124 | ru: "Выбрать все",
125 | es: "Seleccionar todo",
126 | zh: "全选",
127 | },
128 | settings: {
129 | en: "Settings",
130 | ru: "Настройки",
131 | es: "Configuración",
132 | zh: "设置",
133 | },
134 | showing_offers: {
135 | en: "Offers for ",
136 | ru: "Контент для ",
137 | es: "Contenido para ",
138 | zh: "结果自 ",
139 | },
140 | sort_by: {
141 | en: "Sort by",
142 | ru: "Сортировать по",
143 | es: "Ordenar por",
144 | zh: "排序方式",
145 | },
146 | sort_creation_date: {
147 | en: "Creation date",
148 | ru: "Дате создания",
149 | es: "Fecha de creación",
150 | zh: "公开日期",
151 | },
152 | sort_current_price: {
153 | en: "Current price",
154 | ru: "Цене",
155 | es: "Precio",
156 | zh: "当前价格",
157 | },
158 | sort_new_release: {
159 | en: "New release",
160 | ru: "Новинке",
161 | es: "Nuevo lanzamiento",
162 | zh: "最新推出",
163 | },
164 | sort_release_date: {
165 | en: "Release date",
166 | ru: "Дате выхода",
167 | es: "Fecha de lanzamiento",
168 | zh: "推出日期",
169 | },
170 | sort_relevance: {
171 | en: "Relevance",
172 | ru: "Актуальности",
173 | es: "Relevancia",
174 | zh: "相关性",
175 | },
176 | sort_title: {
177 | en: "Title",
178 | ru: "Названию",
179 | es: "Título",
180 | zh: "标题",
181 | },
182 | title: {
183 | en: "Title",
184 | ru: "Название",
185 | es: "Título",
186 | zh: "标题",
187 | },
188 | version: {
189 | en: "Version",
190 | ru: "Версия",
191 | es: "Versión",
192 | zh: "版本",
193 | },
194 | view_on_epic_store: {
195 | en: "Open in Epic Games Store",
196 | ru: "Открыть в магазине Epic Games ",
197 | es: "Abrir en la tienda de Epic Games",
198 | zh: "在Epic游戏商城中查看",
199 | },
200 | };
201 |
--------------------------------------------------------------------------------
/src/util/log.ts:
--------------------------------------------------------------------------------
1 | export class Log {
2 | static readonly DEV = process.env.NODE_ENV === "development";
3 |
4 | public static info(message: any) {
5 | if (Log.DEV) console.log(message);
6 | }
7 |
8 | public static error(message: any) {
9 | if (Log.DEV) console.error(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/util/paths.ts:
--------------------------------------------------------------------------------
1 | export const path = {
2 | to: {
3 | home: "/",
4 | games: "/games",
5 | offers: (namespace?: any) => `/offers/${namespace || ""}`,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/util/query.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLClient } from "graphql-request";
2 | import { getSdk } from "../generated/graphql";
3 |
4 | // Try to read the cors proxy from .env file first.
5 | // If it fails, use the raw graphql endpoint
6 | const ENDPOINT =
7 | process.env.REACT_APP_CORS_PROXY ?? "https://www.epicgames.com/graphql";
8 |
9 | const client = new GraphQLClient(ENDPOINT);
10 | export const sdk = getSdk(client);
11 |
--------------------------------------------------------------------------------
/src/util/storage.ts:
--------------------------------------------------------------------------------
1 | export function writeProp(key: LocalStorageProps, value: any) {
2 | localStorage.setItem(key, value);
3 | }
4 |
5 | export function readProp(key: LocalStorageProps, defaultValue: string) {
6 | let property = localStorage.getItem(key);
7 | if (!property) {
8 | property = defaultValue;
9 | writeProp(key, defaultValue);
10 | }
11 | return property;
12 | }
13 |
14 | export const maxWidth = "lg";
15 |
16 | export type LocalStorageProps =
17 | | "item_per_page"
18 | | "lang"
19 | | "sort_games_by"
20 | | "sort_games_dir"
21 | | "type_filters";
22 |
--------------------------------------------------------------------------------
/src/util/types.ts:
--------------------------------------------------------------------------------
1 | import { Item, OfferType } from "../generated/graphql";
2 |
3 | export interface GameCardData {
4 | id: string;
5 | title: string;
6 | namespace: string;
7 | image?: string | null;
8 | creationDate: Date;
9 | }
10 |
11 | export interface OfferRowData {
12 | id: string;
13 | title: string;
14 | offerType: OfferType;
15 | items: Pick
- [];
16 | image?: string;
17 | }
18 |
19 | export type ValidLanguage = "en" | "es" | "ru" | "zh";
20 | export type ValidSortDirection = "ASC" | "DESC";
21 |
--------------------------------------------------------------------------------
/src/util/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.md" {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2015",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ],
26 | "exclude": [
27 | "workers-site"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/workers/epic-cors-proxy.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-restricted-globals
2 | addEventListener("fetch", event => {
3 | event.respondWith(handleRequest(event.request));
4 | });
5 |
6 | /**
7 | * Respond to the request
8 | * @param {Request} request
9 | */
10 | async function handleRequest(request) {
11 | const patchedRequest = new Request("https://www.epicgames.com/graphql", {
12 | ...request,
13 | headers: new Headers({
14 | // Define customer headers to avoid faulty headers like "Referer"
15 | "Content-type": "application/json"
16 | })
17 | });
18 |
19 | return fetch(patchedRequest.url, patchedRequest).then(res => {
20 | const origin = request.headers.get("origin");
21 | const headers = new Headers(res.headers);
22 | headers.set("Access-Control-Allow-Origin", origin);
23 | headers.set("Access-Control-Allow-Headers", "Content-Type");
24 |
25 | return new Response(res.body, {
26 | ...res,
27 | headers: headers
28 | });
29 | });
30 | }
--------------------------------------------------------------------------------