diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3b305db91..2ff85f1b2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,6 +1,6 @@
{
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": true
+ "source.fixAll.eslint": "explicit"
}
}
diff --git a/package-lock.json b/package-lock.json
index 610d3e076..292bf7576 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "eplay",
"version": "0.1.0",
"dependencies": {
+ "@reduxjs/toolkit": "^2.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -15,13 +16,21 @@
"@types/node": "^16.18.23",
"@types/react": "^18.0.31",
"@types/react-dom": "^18.0.11",
+ "formik": "^2.4.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^6.26.2",
+ "react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.15.0",
+ "styled-components": "^6.1.13",
"typescript": "^4.9.5",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "yup": "^1.6.1"
},
"devDependencies": {
+ "@types/react-router-hash-link": "^2.4.9",
"@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
@@ -2148,6 +2157,24 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -3112,6 +3139,46 @@
}
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
+ "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
+ "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
+ "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3859,11 +3926,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "dev": true
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
- "dev": true,
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
@@ -3963,12 +4035,11 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/react": {
- "version": "18.0.31",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.31.tgz",
- "integrity": "sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww==",
+ "version": "18.3.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.17.tgz",
+ "integrity": "sha512-opAQ5no6LqJNo9TqnxBKsgnkIYHozW9KSTlFVoSUJYh1Fl/sswkEoqIugRSm7tbh6pABtYjGAjW+GOS23j8qbw==",
"dependencies": {
"@types/prop-types": "*",
- "@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
@@ -3980,6 +4051,38 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
+ "node_modules/@types/react-router-hash-link": {
+ "version": "2.4.9",
+ "resolved": "https://registry.npmjs.org/@types/react-router-hash-link/-/react-router-hash-link-2.4.9.tgz",
+ "integrity": "sha512-zl/VMj+lfJZhvjOAQXIlBVPNKSK+/fRG8AUHhlP9++LhlA2ziLeTmbRxIMJI3PCiCTS+W/FosEoDRoNOGH0OzA==",
+ "dev": true,
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router-dom": "^5.3.0"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -3993,11 +4096,6 @@
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="
},
- "node_modules/@types/scheduler": {
- "version": "0.16.3",
- "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
- "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
- },
"node_modules/@types/semver": {
"version": "7.3.13",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz",
@@ -4044,6 +4142,11 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw=="
+ },
"node_modules/@types/testing-library__jest-dom": {
"version": "5.14.5",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz",
@@ -4057,6 +4160,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
+ },
"node_modules/@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
@@ -5403,6 +5511,14 @@
"node": ">= 6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -5838,6 +5954,14 @@
"postcss": "^8.4"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-declaration-sorter": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
@@ -6019,6 +6143,16 @@
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
},
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/css-tree": {
"version": "1.0.0-alpha.37",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
@@ -6206,9 +6340,9 @@
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
"node_modules/csstype": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
- "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -8108,6 +8242,38 @@
"node": ">= 6"
}
},
+ "node_modules/formik": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
+ "integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://opencollective.com/formik"
+ }
+ ],
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "deepmerge": "^2.1.1",
+ "hoist-non-react-statics": "^3.3.0",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "react-fast-compare": "^2.0.1",
+ "tiny-warning": "^1.0.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/formik/node_modules/deepmerge": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
+ "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -8512,7 +8678,6 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
- "dev": true,
"dependencies": {
"react-is": "^16.7.0"
}
@@ -8520,8 +8685,7 @@
"node_modules/hoist-non-react-statics/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/hoopy": {
"version": "0.1.4",
@@ -11623,6 +11787,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -11959,9 +12128,9 @@
}
},
"node_modules/nanoid": {
- "version": "3.3.6",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
- "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
@@ -12608,9 +12777,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.21",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
- "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
+ "version": "8.4.38",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
+ "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
@@ -12619,12 +12788,16 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
- "nanoid": "^3.3.4",
+ "nanoid": "^3.3.7",
"picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -13893,6 +14066,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/property-expr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
+ "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -14202,11 +14380,38 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "node_modules/react-fast-compare": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
+ "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14215,6 +14420,48 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
+ "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
+ "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2",
+ "react-router": "6.26.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/react-router-hash-link": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz",
+ "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "react": ">=15",
+ "react-router-dom": ">=4"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14287,6 +14534,15 @@
}
}
},
+ "node_modules/react-spinners": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz",
+ "integrity": "sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA==",
+ "peerDependencies": {
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14342,6 +14598,19 @@
"node": ">=8"
}
},
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -14468,6 +14737,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
+ },
"node_modules/resolve": {
"version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -14993,6 +15267,11 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -15075,9 +15354,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": {
"node": ">=0.10.0"
}
@@ -15395,6 +15674,33 @@
"webpack": "^5.0.0"
}
},
+ "node_modules/styled-components": {
+ "version": "6.1.13",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz",
+ "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.38",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
"node_modules/stylehacks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
@@ -15410,6 +15716,11 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg=="
+ },
"node_modules/sucrase": {
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.31.0.tgz",
@@ -15807,6 +16118,16 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
+ "node_modules/tiny-case": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
+ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
+ },
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -15839,6 +16160,11 @@
"node": ">=0.6"
}
},
+ "node_modules/toposort": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
+ },
"node_modules/tough-cookie": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
@@ -15913,9 +16239,9 @@
}
},
"node_modules/tslib": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
- "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -16144,6 +16470,14 @@
"requires-port": "^1.0.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
+ "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -17153,6 +17487,28 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yup": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
+ "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
+ "dependencies": {
+ "property-expr": "^2.0.5",
+ "tiny-case": "^1.0.3",
+ "toposort": "^2.0.2",
+ "type-fest": "^2.19.0"
+ }
+ },
+ "node_modules/yup/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index 8a7c7fe75..ab444c735 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@reduxjs/toolkit": "^2.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -10,11 +11,18 @@
"@types/node": "^16.18.23",
"@types/react": "^18.0.31",
"@types/react-dom": "^18.0.11",
+ "formik": "^2.4.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^6.26.2",
+ "react-router-hash-link": "^2.4.3",
"react-scripts": "5.0.1",
+ "react-spinners": "^0.15.0",
+ "styled-components": "^6.1.13",
"typescript": "^4.9.5",
- "web-vitals": "^2.1.4"
+ "web-vitals": "^2.1.4",
+ "yup": "^1.6.1"
},
"scripts": {
"start": "react-scripts start",
@@ -41,6 +49,7 @@
]
},
"devDependencies": {
+ "@types/react-router-hash-link": "^2.4.9",
"@types/styled-components": "^5.1.26",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
diff --git a/public/index.html b/public/index.html
index aa069f27c..e0fab60c1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -25,6 +25,9 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
React App
+
+
+
You need to enable JavaScript to run this app.
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e0534..000000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/App.tsx b/src/App.tsx
index d53c68d8a..ea9264129 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,25 +1,29 @@
-import React from 'react'
-import logo from './logo.svg'
-import './App.css'
+import { Provider } from 'react-redux'
+import { BrowserRouter } from 'react-router-dom'
+
+import { Header } from './components/Header'
+import { GlobalCss } from './styles'
+
+import { Rotas } from './routes'
+import { Footer } from './components/Footer'
+import { store } from './store'
+import { Cart } from './components/Cart'
function App() {
return (
-
+
+
+
)
}
diff --git "a/src/assets/images/1 - Mistborn Primeira Era - O imp\303\251rio final.pdf" "b/src/assets/images/1 - Mistborn Primeira Era - O imp\303\251rio final.pdf"
new file mode 100644
index 000000000..b4d4f4604
Binary files /dev/null and "b/src/assets/images/1 - Mistborn Primeira Era - O imp\303\251rio final.pdf" differ
diff --git a/src/assets/images/banner-homem-aranha.png b/src/assets/images/banner-homem-aranha.png
new file mode 100644
index 000000000..b1efdf7da
Binary files /dev/null and b/src/assets/images/banner-homem-aranha.png differ
diff --git a/src/assets/images/barcode.png b/src/assets/images/barcode.png
new file mode 100644
index 000000000..43a383eba
Binary files /dev/null and b/src/assets/images/barcode.png differ
diff --git a/src/assets/images/carrinho.svg b/src/assets/images/carrinho.svg
new file mode 100644
index 000000000..096e47b83
--- /dev/null
+++ b/src/assets/images/carrinho.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/images/credit-card.png b/src/assets/images/credit-card.png
new file mode 100644
index 000000000..8c97f04c2
Binary files /dev/null and b/src/assets/images/credit-card.png differ
diff --git a/src/assets/images/diablo.png b/src/assets/images/diablo.png
new file mode 100644
index 000000000..1a80c0363
Binary files /dev/null and b/src/assets/images/diablo.png differ
diff --git a/src/assets/images/fechar.png b/src/assets/images/fechar.png
new file mode 100644
index 000000000..ccddafbb9
Binary files /dev/null and b/src/assets/images/fechar.png differ
diff --git a/src/assets/images/fifa.png b/src/assets/images/fifa.png
new file mode 100644
index 000000000..74022352c
Binary files /dev/null and b/src/assets/images/fifa.png differ
diff --git a/src/assets/images/fundo_hogwarts.png b/src/assets/images/fundo_hogwarts.png
new file mode 100644
index 000000000..50fd6eb7a
Binary files /dev/null and b/src/assets/images/fundo_hogwarts.png differ
diff --git a/src/assets/images/image_hogwarts.png b/src/assets/images/image_hogwarts.png
new file mode 100644
index 000000000..e0b11bd88
Binary files /dev/null and b/src/assets/images/image_hogwarts.png differ
diff --git a/src/assets/images/logo.svg b/src/assets/images/logo.svg
new file mode 100644
index 000000000..7962d3ba9
--- /dev/null
+++ b/src/assets/images/logo.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/images/play.png b/src/assets/images/play.png
new file mode 100644
index 000000000..e911712c2
Binary files /dev/null and b/src/assets/images/play.png differ
diff --git a/src/assets/images/resident.png b/src/assets/images/resident.png
new file mode 100644
index 000000000..d22fa6aff
Binary files /dev/null and b/src/assets/images/resident.png differ
diff --git a/src/assets/images/star_wars.png b/src/assets/images/star_wars.png
new file mode 100644
index 000000000..2578845cc
Binary files /dev/null and b/src/assets/images/star_wars.png differ
diff --git a/src/assets/images/streetFighter.png b/src/assets/images/streetFighter.png
new file mode 100644
index 000000000..61338d72c
Binary files /dev/null and b/src/assets/images/streetFighter.png differ
diff --git a/src/assets/images/zelda.png b/src/assets/images/zelda.png
new file mode 100644
index 000000000..c962705a0
Binary files /dev/null and b/src/assets/images/zelda.png differ
diff --git a/src/assets/images/zoom.png b/src/assets/images/zoom.png
new file mode 100644
index 000000000..9de1f92e5
Binary files /dev/null and b/src/assets/images/zoom.png differ
diff --git a/src/components/Banner/index.tsx b/src/components/Banner/index.tsx
new file mode 100644
index 000000000..ccde2e851
--- /dev/null
+++ b/src/components/Banner/index.tsx
@@ -0,0 +1,39 @@
+import { Tags } from '../Tags'
+import { Button } from '../Button'
+import { parseToBrl } from '../../utils'
+import { Loader } from '../Loader'
+
+import { useGetFeaturedGameQuery } from '../../services/api'
+
+import * as S from './style'
+
+export const Banner = () => {
+ const { data: game } = useGetFeaturedGameQuery()
+
+ if (!game) {
+ return
+ }
+
+ return (
+
+
+
Destaque do dia
+
+ {game.name}
+
+ De {parseToBrl(game?.prices.old)}
+
+ por apenas {parseToBrl(game?.prices.discount)}
+
+
+
+ Aproveitar
+
+
+
+ )
+}
diff --git a/src/components/Banner/style.ts b/src/components/Banner/style.ts
new file mode 100644
index 000000000..90f025433
--- /dev/null
+++ b/src/components/Banner/style.ts
@@ -0,0 +1,51 @@
+import styled from 'styled-components'
+import { TagsContainer } from '../Tags/style'
+
+export const Image = styled.div`
+ width: 100%;
+ height: 560px;
+ padding: 32px 0 56px 0;
+ display: block;
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ position: relative;
+
+ .container {
+ position: relative;
+ padding-top: 340px;
+ max-height: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ z-index: 1;
+ }
+
+ ${TagsContainer} {
+ position: absolute;
+ top: 0;
+ }
+
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.7);
+ content: '';
+ }
+`
+
+export const Title = styled.h2`
+ font-size: 32px;
+ max-width: 450px;
+`
+
+export const Price = styled.p`
+ font-size: 24px;
+ margin-top: 24px;
+ span {
+ text-decoration: line-through;
+ }
+`
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
new file mode 100644
index 000000000..343de2ec6
--- /dev/null
+++ b/src/components/Button/index.tsx
@@ -0,0 +1,38 @@
+import { ButtonContainer, ButtonLink } from './style'
+
+export type Props = {
+ type: 'button' | 'link' | 'submit'
+ title: string
+ to?: string
+ onClick?: () => void
+ children: string
+ variant?: 'primary' | 'secundary'
+}
+
+export const Button = ({
+ type,
+ title,
+ to,
+ onClick,
+ children,
+ variant = 'primary'
+}: Props) => {
+ if (type === 'button' || type === 'submit') {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/Button/style.ts b/src/components/Button/style.ts
new file mode 100644
index 000000000..d4956bf9f
--- /dev/null
+++ b/src/components/Button/style.ts
@@ -0,0 +1,27 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+import { Link } from 'react-router-dom'
+import { Props } from '.'
+
+export const ButtonContainer = styled.button`
+ border: 2px solid
+ ${(props) => (props.variant === 'primary' ? colors.green : colors.white)};
+ color: ${colors.white};
+ border-radius: 8px;
+ background: ${(props) =>
+ props.variant === 'primary' ? colors.green : 'transparent'};
+ padding: 8px 16px;
+ font-size: 16px;
+ font-weight: bold;
+ cursor: pointer;
+`
+
+export const ButtonLink = styled(Link)`
+ border: 2px solid ${colors.white};
+ color: ${colors.white};
+ border-radius: 8px;
+ background: transparent;
+ padding: 8px 16px;
+ font-size: 16px;
+ font-weight: bold;
+`
diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx
new file mode 100644
index 000000000..ae75880d6
--- /dev/null
+++ b/src/components/Card/index.tsx
@@ -0,0 +1,15 @@
+import { Container } from './style'
+
+type Props = {
+ children: JSX.Element
+ title: string
+}
+
+export const Card = ({ children, title }: Props) => {
+ return (
+
+ {title}
+ {children}
+
+ )
+}
diff --git a/src/components/Card/style.ts b/src/components/Card/style.ts
new file mode 100644
index 000000000..2622c301c
--- /dev/null
+++ b/src/components/Card/style.ts
@@ -0,0 +1,30 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+
+export const Container = styled.div`
+ border-radius: 8px;
+ background-color: ${colors.gray};
+ padding: 24px;
+ margin-bottom: 40px;
+
+ h2,
+ h3 {
+ font-size: 18px;
+ font-weight: 700;
+ color: ${colors.white};
+ margin-bottom: 24px;
+ }
+
+ h3 {
+ margin-top: 24px;
+ }
+
+ p {
+ font-size: 14px;
+ line-height: 22px;
+ }
+
+ .margin-top {
+ margin-top: 16px;
+ }
+`
diff --git a/src/components/Cart/index.tsx b/src/components/Cart/index.tsx
new file mode 100644
index 000000000..5acefb6cb
--- /dev/null
+++ b/src/components/Cart/index.tsx
@@ -0,0 +1,63 @@
+import { useDispatch, useSelector } from 'react-redux'
+import { useNavigate } from 'react-router-dom'
+
+import { Tags } from '../Tags'
+import { Button } from '../Button'
+import { getTotalPrice, parseToBrl } from '../../utils'
+
+import { RootReducer } from '../../store'
+import { close, remove } from '../../store/reducers/cart'
+
+import * as S from './style'
+
+export const Cart = () => {
+ const { isOpen, items } = useSelector((state: RootReducer) => state.cart)
+ const navigate = useNavigate()
+
+ const dispatch = useDispatch()
+
+ const closeCart = () => {
+ dispatch(close())
+ }
+
+ const removeItem = (id: number) => {
+ dispatch(remove(id))
+ }
+
+ const goToCheckout = () => {
+ navigate('/checkout')
+ closeCart()
+ }
+
+ return (
+
+
+
+
+ {items.map((item) => (
+
+
+
+
{item.name}
+
{item.details.category}
+
{item.details.system}
+
{parseToBrl(item.prices.current)}
+
+ removeItem(item.id)} type="button" />
+
+ ))}
+
+
+ {items.length} jogo(s) no carrinho
+
+ Total de {parseToBrl(getTotalPrice(items))}
+ em até 6x sem juros
+
+
+
+ Continuar a compra
+
+
+
+ )
+}
diff --git a/src/components/Cart/style.ts b/src/components/Cart/style.ts
new file mode 100644
index 000000000..40e0fb16b
--- /dev/null
+++ b/src/components/Cart/style.ts
@@ -0,0 +1,87 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+import { ButtonContainer } from '../Button/style'
+import { TagsContainer } from '../Tags/style'
+
+import fechar from '../../assets/images/fechar.png'
+
+export const Overlay = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: #000;
+ opacity: 0.7;
+`
+
+export const CartContainer = styled.div`
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 1;
+ display: none;
+ justify-content: end;
+
+ &.isOpen {
+ display: flex;
+ }
+`
+
+export const Sidebar = styled.aside`
+ padding: 40px 16px;
+ background-color: ${colors.gray};
+ height: 100%;
+ width: 360px;
+ z-index: 1;
+
+ ${ButtonContainer} {
+ width: 100%;
+ margin-top: 24px;
+ }
+`
+
+export const CartItem = styled.li`
+ display: flex;
+ border-bottom: 1px solid ${colors.lightgray};
+ padding: 8px 0;
+ position: relative;
+
+ img {
+ height: 80px;
+ width: 80px;
+ margin-right: 23px;
+ object-fit: cover;
+ }
+
+ ${TagsContainer} {
+ margin: 8px 8px 16px 0;
+ }
+
+ button {
+ background-image: url(${fechar});
+ width: 16px;
+ height: 16px;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ position: absolute;
+ top: 8px;
+ right: 0;
+ }
+`
+export const Quantity = styled.p`
+ margin: 32px 0 16px 0;
+ font-size: 16px;
+`
+
+export const Prices = styled.p`
+ font-size: 14px;
+ span {
+ display: block;
+ font-size: 12px;
+ color: ${colors.lightgray};
+ }
+`
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 000000000..7c638622e
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,77 @@
+import * as S from './style'
+
+const current = new Date().getFullYear()
+
+export const Footer = () => (
+
+
+
+ Categorias
+
+
+
+ RPG
+
+
+
+
+ Ação
+
+
+
+
+ Esportes
+
+
+
+
+ Simulação
+
+
+
+
+ Luta
+
+
+
+
+
+ Acesso rápido
+
+
+
+ Promoções
+
+
+
+
+ Em breve
+
+
+
+
+
{current} - © E-PLAY Todos os direitos reservados
+
+
+)
diff --git a/src/components/Footer/style.ts b/src/components/Footer/style.ts
new file mode 100644
index 000000000..4d704fa0e
--- /dev/null
+++ b/src/components/Footer/style.ts
@@ -0,0 +1,30 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+import { HashLink } from 'react-router-hash-link'
+
+export const ContainerFooter = styled.footer`
+ background-color: ${colors.gray};
+ font-size: 14px;
+ padding-bottom: 32px;
+ margin-top: 40px;
+`
+
+export const SectionFooter = styled.section`
+ margin-bottom: 64px;
+`
+
+export const Title = styled.h4`
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 16px;
+`
+
+export const Links = styled.ul`
+ display: flex;
+ flex-wrap: wrap;
+`
+
+export const Link = styled(HashLink)`
+ color: ${colors.lightgray};
+ margin-right: 8px;
+`
diff --git a/src/components/Gallery/index.tsx b/src/components/Gallery/index.tsx
new file mode 100644
index 000000000..a11215493
--- /dev/null
+++ b/src/components/Gallery/index.tsx
@@ -0,0 +1,91 @@
+import { useState } from 'react'
+
+import { Section } from '../Section'
+
+import { GalleryItem } from '../../pages/Home'
+
+import zoom from '../../assets/images/zoom.png'
+import play from '../../assets/images/play.png'
+import closeIcon from '../../assets/images/fechar.png'
+
+import * as S from './style'
+
+type Props = {
+ defaultCover: string
+ nome: string
+ items: GalleryItem[]
+}
+
+interface ModalState extends GalleryItem {
+ isVisible: boolean
+}
+
+export const Gallery = ({ defaultCover, nome, items }: Props) => {
+ const [modal, setModal] = useState({
+ isVisible: false,
+ type: 'image',
+ url: ''
+ })
+
+ const getMediaCover = (item: GalleryItem) => {
+ if (item.type === 'image') return item.url
+ return defaultCover
+ }
+ const getMediaIcon = (item: GalleryItem) => {
+ if (item.type === 'image') return zoom
+ return play
+ }
+
+ const closeModal = () => {
+ setModal({
+ isVisible: false,
+ type: 'image',
+ url: ''
+ })
+ }
+
+ return (
+ <>
+
+
+ {items.map((media, index) => (
+ {
+ setModal({
+ isVisible: true,
+ type: media.type,
+ url: media.url
+ })
+ }}
+ >
+
+
+
+
+
+ ))}
+
+
+ closeModal()}
+ >
+
+
+ {nome}
+
+
+ {modal.type === 'image' ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ )
+}
diff --git a/src/components/Gallery/style.ts b/src/components/Gallery/style.ts
new file mode 100644
index 000000000..b363938ac
--- /dev/null
+++ b/src/components/Gallery/style.ts
@@ -0,0 +1,103 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+
+export const Items = styled.ul`
+ @media only screen and (min-width: 600px) {
+ display: flex;
+ }
+`
+
+export const Action = styled.div`
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.73);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ opacity: 0;
+ cursor: pointer;
+ transition: opacity 0.5s ease;
+ cursor: zoom-in;
+`
+
+export const Item = styled.li`
+ position: relative;
+ border: 2px solid ${colors.white};
+ border-radius: 8px;
+ margin-bottom: 16px;
+ overflow: hidden;
+ width: 100%;
+ height: 300px;
+
+ &:hover {
+ ${Action} {
+ opacity: 1;
+ transition: opacity 0.5s ease;
+ }
+ }
+
+ @media only screen and (min-width: 600px) {
+ width: 150px;
+ height: 150px;
+ margin-right: 16px;
+ }
+
+ > img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+`
+
+export const Modal = styled.div`
+ position: fixed;
+ top: 0;
+ height: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 1;
+ background-color: rgba(0, 0, 0, 0.73);
+ display: none;
+ align-items: center;
+ justify-content: center;
+
+ &.is-visible {
+ display: flex;
+ }
+`
+
+export const ModalContent = styled.div`
+ max-width: 960px;
+
+ header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ font-size: 18px;
+ font-weight: 700;
+
+ > img {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ > img {
+ width: 100%;
+ }
+
+ img,
+ iframe {
+ display: block;
+ max-width: 100%;
+ }
+
+ iframe {
+ width: 100%;
+ height: 450px;
+ }
+`
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
new file mode 100644
index 000000000..fc01048b7
--- /dev/null
+++ b/src/components/Header/index.tsx
@@ -0,0 +1,102 @@
+import { Link } from 'react-router-dom'
+import { useDispatch, useSelector } from 'react-redux'
+import { useState } from 'react'
+
+import { RootReducer } from '../../store'
+import { open } from '../../store/reducers/cart'
+
+import logo from '../../assets/images/logo.svg'
+import cartIcon from '../../assets/images/carrinho.svg'
+
+import * as S from './style'
+import { HashLink } from 'react-router-hash-link'
+
+export const Header = () => {
+ const dispatch = useDispatch()
+ const { items } = useSelector((state: RootReducer) => state.cart)
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
+
+ const openCart = () => {
+ dispatch(open())
+ }
+
+ return (
+
+
+
+
setIsMenuOpen(!isMenuOpen)}>
+
+
+
+
+
+
+
+
+
+
+
+ Categorias
+
+
+
+
+ Em breve
+
+
+
+
+ Promoções
+
+
+
+
+
+
+ {items.length} - produto(s)
+
+
+
+
+
+
+ setIsMenuOpen(false)}
+ >
+ Categorias
+
+
+
+ setIsMenuOpen(false)}
+ >
+ Em breve
+
+
+
+ setIsMenuOpen(false)}
+ >
+ Promoções
+
+
+
+
+
+ )
+}
diff --git a/src/components/Header/style.ts b/src/components/Header/style.ts
new file mode 100644
index 000000000..f97a876ed
--- /dev/null
+++ b/src/components/Header/style.ts
@@ -0,0 +1,96 @@
+import styled from 'styled-components'
+import { breakpoint, colors } from '../../styles'
+
+export const Links = styled.ul`
+ display: flex;
+ margin-left: 40px;
+
+ @media (max-width: ${breakpoint.tablet}) {
+ margin-left: 0;
+ display: block;
+ }
+`
+
+export const HeaderBar = styled.header`
+ background-color: ${colors.gray};
+ padding: 24px;
+ margin-bottom: 80px;
+ border-radius: 16px;
+ }
+`
+
+export const HeaderRow = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ > div {
+ display: flex;
+ align-items: center;
+
+ @media (max-width: ${breakpoint.tablet}) {
+ flex: 1;
+ justify-content: space-between;
+
+ ${Links} {
+ display: none;
+ }
+ }
+`
+
+export const NavMobile = styled.nav`
+ display: none;
+
+ &.is-open {
+ display: block;
+ }
+`
+
+export const LinkItem = styled.li`
+ color: ${colors.white};
+ margin-left: 16px;
+
+ @media (max-width: ${breakpoint.tablet}) {
+ margin-right: 0;
+
+ a {
+ padding: 16px 0;
+ display: block;
+ text-align: center;
+ }
+ }
+`
+
+export const CartButton = styled.a`
+ display: flex;
+ cursor: pointer;
+
+ img {
+ margin-left: 16px;
+ }
+
+ @media (max-width: ${breakpoint.tablet}) {
+ span {
+ display: none;
+ }
+ }
+`
+
+export const Hamburguer = styled.div`
+ width: 32px;
+ display: fex;
+ flex-direction: column;
+ cursor: pointer;
+
+ span {
+ height: 2px;
+ display: block;
+ width: 100%;
+ background-color: ${colors.white};
+ margin-bottom: 4px;
+ }
+
+ @media (min-width: ${breakpoint.tablet}) {
+ display: none;
+ }
+`
diff --git a/src/components/Hero/index.tsx b/src/components/Hero/index.tsx
new file mode 100644
index 000000000..bff690710
--- /dev/null
+++ b/src/components/Hero/index.tsx
@@ -0,0 +1,53 @@
+import { useDispatch } from 'react-redux'
+
+import { Button } from '../Button'
+import { Tags } from '../Tags'
+
+import { Game } from '../../pages/Home'
+import { add, open } from '../../store/reducers/cart'
+
+import * as S from './style'
+import { parseToBrl } from '../../utils'
+
+type Props = {
+ game: Game
+}
+
+export const Hero = ({ game }: Props) => {
+ const dispatch = useDispatch()
+
+ const addToCart = () => {
+ dispatch(add(game))
+ dispatch(open())
+ }
+
+ return (
+
+
+
+ {game.details.system}
+ {game.details.category}
+
+
+ {game.name}
+
+ {game.prices.discount && (
+ De {parseToBrl(game?.prices.old)}
+ )}
+ {game.prices.current && <>Por {parseToBrl(game?.prices.current)}>}
+
+ {game.prices.current && (
+
+ Adicionar ao carrinho
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/Hero/style.ts b/src/components/Hero/style.ts
new file mode 100644
index 000000000..b3060e6fd
--- /dev/null
+++ b/src/components/Hero/style.ts
@@ -0,0 +1,59 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+import { TagsContainer } from '../Tags/style'
+
+export const Banner = styled.div`
+ position: relative;
+ display: block;
+ height: 480px;
+ width: 100%;
+
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 100%;
+
+ &::after {
+ position: absolute;
+ background-color: #000;
+ height: 100%;
+ width: 100%;
+ top: 0;
+ left: 0;
+ content: '';
+ opacity: 0.56;
+ }
+
+ .container {
+ z-index: 1;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+
+ ${TagsContainer} {
+ margin-right: 8px;
+ }
+ }
+`
+
+export const Infos = styled.div`
+ padding: 16px;
+ background-color: ${colors.black};
+ font-weight: 700;
+ max-width: 290px;
+
+ h2 {
+ font-size: 32px;
+ }
+
+ p {
+ font-size: 18px;
+ margin: 16px 0;
+
+ span {
+ display: block;
+ text-decoration: line-through;
+ }
+ }
+`
diff --git a/src/components/Loader/index.tsx b/src/components/Loader/index.tsx
new file mode 100644
index 000000000..7506701a4
--- /dev/null
+++ b/src/components/Loader/index.tsx
@@ -0,0 +1,11 @@
+import { PacmanLoader } from 'react-spinners'
+
+import { colors } from '../../styles'
+
+import { Container } from './style'
+
+export const Loader = () => (
+
+
+
+)
diff --git a/src/components/Loader/style.ts b/src/components/Loader/style.ts
new file mode 100644
index 000000000..1ff84e4d6
--- /dev/null
+++ b/src/components/Loader/style.ts
@@ -0,0 +1,8 @@
+import { styled } from 'styled-components'
+
+export const Container = styled.div`
+ min-height: 360px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`
diff --git a/src/components/Product/index.tsx b/src/components/Product/index.tsx
new file mode 100644
index 000000000..e56061a79
--- /dev/null
+++ b/src/components/Product/index.tsx
@@ -0,0 +1,47 @@
+import { Tags } from '../Tags'
+
+import * as S from './style'
+
+export type Props = {
+ title: string
+ category: string
+ system: string
+ description: string
+ infos: string[]
+ image: string
+ id: number
+}
+
+export const Product = ({
+ title,
+ category,
+ system,
+ description,
+ infos,
+ image,
+ id
+}: Props) => {
+ const getDescription = (text: string) => {
+ if (text.length > 95) {
+ return text.slice(0, 92) + '...'
+ }
+ return text
+ }
+ return (
+
+
+
+ {infos.map((info) => (
+ {info}
+ ))}
+
+ {title}
+ {category}
+ {system}
+ {getDescription(description)}
+
+ )
+}
diff --git a/src/components/Product/style.ts b/src/components/Product/style.ts
new file mode 100644
index 000000000..76320536e
--- /dev/null
+++ b/src/components/Product/style.ts
@@ -0,0 +1,44 @@
+import styled from 'styled-components'
+import { Link } from 'react-router-dom'
+import { colors } from '../../styles'
+import { TagsContainer } from '../Tags/style'
+
+export const Card = styled(Link)`
+ position: relative;
+ display: block;
+ height: 100%;
+ margin-bottom: 8px;
+ padding: 8px 8px 16px 8px;
+ border-radius: 8px;
+ background-color: ${colors.gray};
+ color: ${colors.white};
+ text-decoration: none;
+
+ img {
+ display: bolock;
+ width: 100%;
+ height: 250px;
+ object-fit: cover;
+ }
+
+ ${TagsContainer} {
+ margin-right: 8px;
+ }
+`
+
+export const Title = styled.h3`
+ font-size: 16px;
+ font-weiight: bold;
+ margin: 16px 0 8px 0;
+`
+
+export const Description = styled.p`
+ margin-top: 16px;
+ font-size: 14px;
+`
+
+export const Infos = styled.div`
+ top: 16px;
+ right: 16px;
+ position: absolute;
+`
diff --git a/src/components/ProductList/index.tsx b/src/components/ProductList/index.tsx
new file mode 100644
index 000000000..1a1837017
--- /dev/null
+++ b/src/components/ProductList/index.tsx
@@ -0,0 +1,68 @@
+import { Game } from '../../pages/Home'
+import { parseToBrl } from '../../utils'
+import { Loader } from '../Loader'
+import { Product } from '../Product'
+
+import * as S from './styles'
+
+export type Props = {
+ title: string
+ background: 'gray' | 'black'
+ games?: Game[]
+ id?: string
+ isLoading: boolean
+}
+
+export const ProductList = ({
+ title,
+ background,
+ games,
+ id,
+ isLoading
+}: Props) => {
+ const getGameTags = (game: Game) => {
+ const tags = []
+
+ if (game.relase_date) {
+ tags.push(game.relase_date)
+ }
+
+ if (game.prices.discount) {
+ tags.push(`${game.prices.discount}%`)
+ }
+
+ if (game.prices.current) {
+ tags.push(parseToBrl(game.prices.current))
+ }
+
+ return tags
+ }
+
+ if (isLoading) {
+ return
+ }
+
+ return (
+
+
+
{title}
+
+ {games &&
+ games.map((game) => (
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/components/ProductList/styles.ts b/src/components/ProductList/styles.ts
new file mode 100644
index 000000000..a96750a66
--- /dev/null
+++ b/src/components/ProductList/styles.ts
@@ -0,0 +1,35 @@
+import styled from 'styled-components'
+
+import { Props } from '.'
+import { breakpoint, colors } from '../../styles'
+import { Card } from '../Product/style'
+
+export const Container = styled.section<
+ Omit
+>`
+ padding-bottom: 32px;
+ background-color: ${(props) =>
+ props.background === 'black' ? colors.black : colors.gray};
+
+ ${Card} {
+ background-color: ${(props) =>
+ props.background === 'black' ? colors.gray : colors.black};
+ }
+`
+
+export const List = styled.ul`
+ display: grid;
+ gap: 40px 24px;
+
+ @media only screen and (min-width: ${breakpoint.tablet}) {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ @media only screen and (min-width: ${breakpoint.desktop}) {
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ }
+`
+
+export const Title = styled.h2`
+ margin-bottom: 40px;
+`
diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx
new file mode 100644
index 000000000..6c5f84400
--- /dev/null
+++ b/src/components/Section/index.tsx
@@ -0,0 +1,16 @@
+import * as S from './style'
+
+export type Props = {
+ title: string
+ background: 'black' | 'gray'
+ children: JSX.Element
+}
+
+export const Section = ({ title, background, children }: Props) => (
+
+
+ {title}
+ {children}
+
+
+)
diff --git a/src/components/Section/style.ts b/src/components/Section/style.ts
new file mode 100644
index 000000000..404da20df
--- /dev/null
+++ b/src/components/Section/style.ts
@@ -0,0 +1,27 @@
+import styled from 'styled-components'
+
+import { Props } from '.'
+import { colors } from '../../styles'
+import { Card } from '../Product/style'
+
+export const Container = styled.section>`
+ padding-bottom: 32px;
+ background-color: ${(props) =>
+ props.background === 'black' ? colors.black : colors.gray};
+
+ ${Card} {
+ background-color: ${(props) =>
+ props.background === 'black' ? colors.gray : colors.black};
+ }
+
+ p,
+ ul {
+ font-size: 14px;
+ line-height: 22px;
+ max-width: 640px;
+ }
+`
+
+export const Title = styled.h2`
+ margin-bottom: 40px;
+`
diff --git a/src/components/Tags/index.tsx b/src/components/Tags/index.tsx
new file mode 100644
index 000000000..8f8ea8e24
--- /dev/null
+++ b/src/components/Tags/index.tsx
@@ -0,0 +1,10 @@
+import { TagsContainer } from './style'
+
+export type Props = {
+ size?: 'small' | 'big'
+ children: string
+}
+
+export const Tags = ({ children, size = 'small' }: Props) => (
+ {children}
+)
diff --git a/src/components/Tags/style.ts b/src/components/Tags/style.ts
new file mode 100644
index 000000000..bce4c079a
--- /dev/null
+++ b/src/components/Tags/style.ts
@@ -0,0 +1,13 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+import { Props } from '.'
+
+export const TagsContainer = styled.div`
+ background-color: ${colors.green};
+ color: ${colors.white};
+ padding: ${(props) => (props.size === 'big' ? '8px 16px' : '4px 6px')};
+ border-radius: 8px;
+ font-size: ${(props) => (props.size === 'big' ? '16px' : '10px')};
+ font-weight: bold;
+ display: inline-block;
+`
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index ec2585e8c..000000000
--- a/src/index.css
+++ /dev/null
@@ -1,13 +0,0 @@
-body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
- monospace;
-}
diff --git a/src/index.tsx b/src/index.tsx
index 1cc146d1e..5e4a391bf 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,6 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
-import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
diff --git a/src/logo.svg b/src/logo.svg
deleted file mode 100644
index 9dfc1c058..000000000
--- a/src/logo.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/pages/Categorias/index.tsx b/src/pages/Categorias/index.tsx
new file mode 100644
index 000000000..ecab36a68
--- /dev/null
+++ b/src/pages/Categorias/index.tsx
@@ -0,0 +1,60 @@
+import { ProductList } from '../../components/ProductList'
+
+import {
+ useGetActionGamesQuery,
+ useGetFightGameQuery,
+ useGetRpgGameQuery,
+ useGetSportGamesQuery,
+ useGetSimulationGameQuery
+} from '../../services/api'
+
+export const Categories = () => {
+ const { data: actionGames, isLoading: isLoadingAction } =
+ useGetActionGamesQuery()
+ const { data: fightGames, isLoading: isLoadingFight } = useGetFightGameQuery()
+ const { data: rpgGames, isLoading: isLoadingRpg } = useGetRpgGameQuery()
+ const { data: simulationGames, isLoading: isLoadingSimulation } =
+ useGetSimulationGameQuery()
+ const { data: sportGames, isLoading: isLoadingSports } =
+ useGetSportGamesQuery()
+
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/pages/Chekout/index.tsx b/src/pages/Chekout/index.tsx
new file mode 100644
index 000000000..e18a3a447
--- /dev/null
+++ b/src/pages/Chekout/index.tsx
@@ -0,0 +1,455 @@
+import { useEffect, useState } from 'react'
+import { useFormik } from 'formik'
+import * as Yup from 'yup'
+import { useSelector } from 'react-redux'
+
+import { Button } from '../../components/Button'
+import { Card } from '../../components/Card'
+
+import { usePurchaseMutation } from '../../services/api'
+import { RootReducer } from '../../store'
+
+import barCode from '../../assets/images/barcode.png'
+import cardImg from '../../assets/images/credit-card.png'
+
+import * as S from './style'
+import { Navigate } from 'react-router-dom'
+import { getTotalPrice, parseToBrl } from '../../utils'
+
+type Installment = {
+ quantity: number
+ amount: number
+ formattedAmount: string
+}
+
+export const Chekout = () => {
+ const [payWithCard, setPayWithCard] = useState(false)
+
+ const [purchase, { data, isSuccess }] = usePurchaseMutation()
+
+ const { items } = useSelector((state: RootReducer) => state.cart)
+
+ const [installments, setInstallments] = useState([])
+
+ const totalPrice = getTotalPrice(items)
+
+ const form = useFormik({
+ initialValues: {
+ fullName: '',
+ email: '',
+ cpf: '',
+ deliveryEmail: '',
+ confirmDeliveryEmail: '',
+ cardOwner: '',
+ cpfCardOwner: '',
+ cardDisplayName: '',
+ cardNumber: '',
+ expiresMonth: '',
+ expiresYear: '',
+ cardCode: '',
+ installments: ''
+ },
+ validationSchema: Yup.object({
+ fullName: Yup.string()
+ .min(5, 'o campo precisa ter ao menos 5 caracteres')
+ .required('O campo é obrigatório'),
+ email: Yup.string()
+ .email('E-mail inválido')
+ .required('O campo é obrigatório'),
+ cpf: Yup.string()
+ .min(14, 'O campo precisa ter 14 caracteres')
+ .max(14, 'O campos precisa ter 14 caracteres')
+ .required('O campo é obrigatório'),
+ deliveryEmail: Yup.string()
+ .email('E-mail inválido')
+ .required('O campo é obrigatório'),
+ confirmDeliveryEmail: Yup.string()
+ .oneOf([Yup.ref('deliveryEmail')], 'Os e-mails são diferentes')
+ .required('O campo é obrigatório'),
+
+ cardOwner: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ cpfCardOwner: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ cardDisplayName: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ cardNumber: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ expiresMonth: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ expiresYear: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ cardCode: Yup.string().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ ),
+ installments: Yup.number().when((values, schema) =>
+ payWithCard ? schema.required('O campo é obrigatório') : schema
+ )
+ }),
+ onSubmit: (values) => {
+ purchase({
+ billing: {
+ document: values.cpf,
+ email: values.email,
+ name: values.fullName
+ },
+ delivery: {
+ email: values.deliveryEmail
+ },
+ payment: {
+ installments: 1,
+ card: {
+ active: payWithCard,
+ code: Number(values.cardCode),
+ name: values.cardDisplayName,
+ number: values.cardNumber,
+ owner: {
+ document: values.cpfCardOwner,
+ name: values.cardOwner
+ },
+ expires: {
+ month: 1,
+ year: 2025
+ }
+ }
+ },
+ products: [
+ {
+ id: 1,
+ price: 10
+ }
+ ]
+ })
+ }
+ })
+
+ console.log(form)
+
+ const checkInputHasError = (fieldName: string) => {
+ const isTouched = fieldName in form.touched
+ const isInvalid = fieldName in form.errors
+ const hasError = isTouched && isInvalid
+
+ return hasError
+ }
+
+ useEffect(() => {
+ const calculateInstallments = () => {
+ const installmentsArray: Installment[] = []
+
+ for (let i = 1; i <= 6; i++) {
+ installmentsArray.push({
+ quantity: i,
+ amount: totalPrice / i,
+ formattedAmount: parseToBrl(totalPrice / i)
+ })
+ }
+
+ return installmentsArray
+ }
+ if (totalPrice > 0) {
+ setInstallments(calculateInstallments())
+ }
+ }, [totalPrice])
+
+ if (items.length === 0) {
+ return
+ }
+
+ return (
+
+ {isSuccess ? (
+
+ <>
+
+ É com satisfação que informamos que recebemos seu pedido com
+ sucesso!
+
+ Abaixo estão os detalhes da sua compra:
+ Número do pedido: {data.orderId}
+
+ Forma de pagamento:{' '}
+ {payWithCard ? 'Cartão de Crédito' : 'Boleto Bancário'}
+
+
+ Caso tenha optado pelo pagamento via boleto bancário, lembre-se de
+ que a confirmação pode levar até 3 dias úteis. Após a aprovação do
+ pagamento, enviaremos um e-mail contendo o código de ativação do
+ jogo.
+
+
+ Se você optou pelo pagamento com cartão de crédito, a liberação do
+ código de ativação ocorrerá após a aprovação da transação pela
+ operadora do cartão. Você receberá o código no e-mail cadastrado
+ em nossa loja.
+
+
+ Pedimos que verifique sua caixa de entrada e a pasta de spam para
+ garantir que receba nossa comunicação. Caso tenha alguma dúvida ou
+ necessite de mais informações, por favor, entre em contato conosco
+ através dos nossos canais de atendimento ao cliente.
+
+
+ Agradecemos por escolher a EPLAY e esperamos que desfrute do seu
+ jogo!
+
+ >
+
+ ) : (
+
+ )}
+
+ )
+}
diff --git a/src/pages/Chekout/style.ts b/src/pages/Chekout/style.ts
new file mode 100644
index 000000000..1948d9bea
--- /dev/null
+++ b/src/pages/Chekout/style.ts
@@ -0,0 +1,68 @@
+import styled from 'styled-components'
+import { colors } from '../../styles'
+
+type TabButtonProps = {
+ isActive: boolean
+}
+
+type InputGroupProps = {
+ maxWidth?: string
+}
+
+type RowProps = {
+ marginTop?: string
+}
+
+export const Row = styled.div`
+ display: flex;
+ column-gap: 24px;
+ margin-top: ${(props) => props.marginTop || '0'};
+`
+
+export const InfoGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: auto;
+
+ max-width: ${(props) => props.maxWidth || 'auto'};
+
+ label {
+ font-size: 14px;
+ margin: 8px 0;
+ }
+
+ option {
+ color: ${colors.black};
+ }
+
+ input,
+ select {
+ height: 32px;
+ padding: 0 8px;
+ border: 1px solid ${colors.white};
+ background-color: ${colors.white};
+ color: ${colors.black};
+ widht: 100%;
+
+ &.error {
+ border: 1px solid red;
+ }
+ }
+`
+
+export const TabButton = styled.button`
+ background-color: ${(props) =>
+ props.isActive ? colors.green : colors.black};
+ padding: 8px;
+ border-radius: 8px;
+ border: none;
+ height: 32px;
+ margin-right: 16px;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 700;
+
+ img {
+ margin-right: 8px;
+ }
+`
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx
new file mode 100644
index 000000000..885acfc06
--- /dev/null
+++ b/src/pages/Home/index.tsx
@@ -0,0 +1,57 @@
+import { Banner } from '../../components/Banner'
+import { ProductList } from '../../components/ProductList'
+
+import { useGetOnSaleQuery, useGetSoonQuery } from '../../services/api'
+
+export interface GalleryItem {
+ type: 'image' | 'video'
+ url: string
+}
+
+export type Game = {
+ id: number
+ name: string
+ description: string
+ relase_date?: string
+ prices: {
+ discount?: number
+ old?: number
+ current?: number
+ }
+ details: {
+ category: string
+ system: string
+ developer: string
+ publisher: string
+ languages: string[]
+ }
+ media: {
+ thumbnail: string
+ cover: string
+ gallery: GalleryItem[]
+ }
+}
+
+export const Home = () => {
+ const { data: onSaleGames, isLoading: isLoadingSale } = useGetOnSaleQuery()
+ const { data: soonGames, isLoading: isLoadingSoon } = useGetSoonQuery()
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/pages/Product/index.tsx b/src/pages/Product/index.tsx
new file mode 100644
index 000000000..3c4157189
--- /dev/null
+++ b/src/pages/Product/index.tsx
@@ -0,0 +1,53 @@
+import { useParams } from 'react-router-dom'
+
+import { Hero } from '../../components/Hero'
+import { Section } from '../../components/Section'
+import { Gallery } from '../../components/Gallery'
+import { Loader } from '../../components/Loader'
+
+import { useGetGameQuery } from '../../services/api'
+
+type GameParams = {
+ id: string
+}
+
+export const Product = () => {
+ const { id } = useParams() as GameParams
+
+ const { data: game } = useGetGameQuery(id)
+
+ if (!game) {
+ return
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Plataforma: {game?.details.system}
+
+
+ Desenvolvedor: {game?.details.developer}
+
+
+ Editora: {game?.details.publisher}
+
+
+ Idiomas: O jogo oferece suporte a diversos idiomas, incluindo
+ {game?.details.languages.join(', ')}
+
+
+
+
+ >
+ )
+}
diff --git a/src/routes.tsx b/src/routes.tsx
new file mode 100644
index 000000000..7df01743d
--- /dev/null
+++ b/src/routes.tsx
@@ -0,0 +1,14 @@
+import { Routes, Route } from 'react-router-dom'
+import { Home } from './pages/Home'
+import { Categories } from './pages/Categorias'
+import { Product } from './pages/Product'
+import { Chekout } from './pages/Chekout'
+
+export const Rotas = () => (
+
+ } />
+ } />
+ } />
+ } />
+
+)
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 000000000..75c074336
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,94 @@
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
+
+import { Game } from '../pages/Home'
+
+type Product = {
+ id: number
+ price: number
+}
+
+type PurchasePayload = {
+ products: Product[]
+ billing: {
+ name: string
+ email: string
+ document: string
+ }
+ delivery: {
+ email: string
+ }
+ payment: {
+ card: {
+ active: boolean
+ owner?: {
+ name: string
+ document: string
+ }
+ name: string
+ number: string
+ expires: {
+ month: number
+ year: number
+ }
+ code: number
+ }
+ installments: number
+ }
+}
+
+const api = createApi({
+ baseQuery: fetchBaseQuery({
+ baseUrl: 'https://ebac-fake-api.vercel.app/api/eplay/checkout'
+ }),
+ endpoints: (builder) => ({
+ getFeaturedGame: builder.query({
+ query: () => 'destaque'
+ }),
+ getOnSale: builder.query({
+ query: () => 'promocoes'
+ }),
+ getSoon: builder.query({
+ query: () => 'em-breve'
+ }),
+ getActionGames: builder.query({
+ query: () => 'acao'
+ }),
+ getSportGames: builder.query({
+ query: () => 'esportes'
+ }),
+ getFightGame: builder.query({
+ query: () => 'luta'
+ }),
+ getRpgGame: builder.query({
+ query: () => 'rpg'
+ }),
+ getSimulationGame: builder.query({
+ query: () => 'simulacao'
+ }),
+ getGame: builder.query({
+ query: (id) => `jogos/${id}`
+ }),
+ purchase: builder.mutation({
+ query: (body) => ({
+ url: 'checkout',
+ method: 'Post',
+ body
+ })
+ })
+ })
+})
+
+export const {
+ useGetFeaturedGameQuery,
+ useGetOnSaleQuery,
+ useGetSoonQuery,
+ useGetActionGamesQuery,
+ useGetRpgGameQuery,
+ useGetSportGamesQuery,
+ useGetSimulationGameQuery,
+ useGetFightGameQuery,
+ useGetGameQuery,
+ usePurchaseMutation
+} = api
+
+export default api
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 000000000..db7d9ee69
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,16 @@
+import { configureStore } from '@reduxjs/toolkit'
+
+import api from '../services/api'
+
+import cartReducer from './reducers/cart'
+
+export const store = configureStore({
+ reducer: {
+ cart: cartReducer,
+ [api.reducerPath]: api.reducer
+ },
+ middleware: (getDefaultMiddleware) =>
+ getDefaultMiddleware().concat(api.middleware)
+})
+
+export type RootReducer = ReturnType
diff --git a/src/store/reducers/cart.ts b/src/store/reducers/cart.ts
new file mode 100644
index 000000000..73aa9fca6
--- /dev/null
+++ b/src/store/reducers/cart.ts
@@ -0,0 +1,41 @@
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+import { Game } from '../../pages/Home'
+
+type CartState = {
+ items: Game[]
+ isOpen: boolean
+}
+
+const initialState: CartState = {
+ items: [],
+ isOpen: false
+}
+
+const cartSlice = createSlice({
+ name: 'cart',
+ initialState,
+ reducers: {
+ add: (state, action: PayloadAction) => {
+ const game = state.items.find((item) => item.id === action.payload.id)
+
+ if (!game) {
+ state.items.push(action.payload)
+ } else {
+ alert('O jogo já foi adicionado ao carrinho !')
+ }
+ },
+ remove: (state, action: PayloadAction) => {
+ state.items = state.items.filter((item) => item.id !== action.payload)
+ },
+ open: (state) => {
+ state.isOpen = true
+ },
+ close: (state) => {
+ state.isOpen = false
+ }
+ }
+})
+
+export const { add, close, open, remove } = cartSlice.actions
+export default cartSlice.reducer
diff --git a/src/styles.ts b/src/styles.ts
new file mode 100644
index 000000000..a5697b942
--- /dev/null
+++ b/src/styles.ts
@@ -0,0 +1,41 @@
+import { createGlobalStyle } from 'styled-components'
+
+export const colors = {
+ black: '#111111',
+ gray: '#333',
+ green: '#10AC84',
+ white: '#EEEEEE',
+ lightgray: '#A3A3A3'
+}
+
+export const breakpoint = {
+ desktop: '1024px',
+ tablet: '768px'
+}
+
+export const GlobalCss = createGlobalStyle`
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: "Roboto", sans-serif;
+ list-style-type: none;
+ text-decoration: none;
+ color: ${colors.white};
+}
+
+body {
+ background-color: ${colors.black};
+}
+
+.container {
+ max-width: 1024px;
+ width: 80%;
+ margin: 0 auto;
+ padding-top: 40px;
+
+ @media only screen and (min-width: ${breakpoint.desktop}) {
+ width: 100%;
+ }
+}
+`
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 000000000..5f6c24e4b
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,17 @@
+import { Game } from '../pages/Home'
+
+export const parseToBrl = (amount = 0) => {
+ return new Intl.NumberFormat('pt-BR', {
+ style: 'currency',
+ currency: 'BRL'
+ }).format(amount)
+}
+
+export const getTotalPrice = (items: Game[]) => {
+ return items.reduce((accumulator, currentItem) => {
+ if (currentItem.prices.current) {
+ return (accumulator += currentItem.prices.current)
+ }
+ return 0
+ }, 0)
+}