diff --git a/.env b/.env
index aa13947..0a6cbcd 100644
--- a/.env
+++ b/.env
@@ -1,9 +1,6 @@
SKIP_PREFLIGHT_CHECK=true
API_GENCOMMENT=/** @generated THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */
-REACT_APP_API_URL=https://api.github.com/graphql
-# Используется для кодогенерации, Apollo Client плагина
-REACT_APP_ACCESS_TOKEN=9221b1b5ba4b0de72eed80b09602ed7b4c73c222
# Firebase section
REACT_APP_FIREBASE_apiKey=AIzaSyABlBQc-tjCRKWwBj8jTTrMiT2M2UKiJpk
@@ -15,3 +12,12 @@ REACT_APP_FIREBASE_messagingSenderId=14406286286
REACT_APP_FIREBASE_appId=1:14406286286:web:58c7c11c2762d36a55c99f
REACT_APP_DEV_STORAGE_URL=https://dev.github-client.gq/dev/temp-stands.html
+
+# Github links
+REACT_APP_API_URL=https://api.github.com/graphql
+REACT_APP_GITHUB_DOMAIN=https://github.com/
+REACT_APP_GITHUB_MAIN=https://github.com/ani-team/github-client
+REACT_APP_GITHUB_FEEDBACK=https://github.com/ani-team/github-client/issues/new
+
+# Используется для кодогенерации, Apollo Client плагина
+REACT_APP_ACCESS_TOKEN=8709f2ca2f263a81f1de65511485c97c70f653f0
diff --git a/.eslintignore b/.eslintignore
index c39cafd..b971172 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,3 +3,5 @@ dist/**
build/**
*.gen.ts
.github
+public/
+.workflows
diff --git a/.eslintrc.js b/.eslintrc.js
index 5e37602..c86d615 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,3 +1,27 @@
+/** Разрешенные импорты (с публичными API) */
+const ALLOWED_PATH_GROUPS = ["shared", "shared/**", "pages", "features", "models"].map(
+ (pattern) => ({
+ pattern,
+ group: "internal",
+ position: "after",
+ }),
+);
+/** Для запрета приватных путей */
+const DENIED_PATH_GROUPS = [
+ // Private imports are prohibited, use public imports instead
+ "app/**",
+ "pages/**",
+ "features/**",
+ "shared/*/**",
+ "models.gen",
+ // Prefer absolute imports instead of relatives (for root modules)
+ "../**/app",
+ "../**/pages",
+ "../**/features",
+ "../**/shared",
+ "../**/models",
+];
+
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
@@ -12,7 +36,7 @@ module.exports = {
browser: true,
es6: true,
},
- plugins: ["react", "@typescript-eslint", "@graphql-eslint"],
+ plugins: ["react", "@typescript-eslint", "@graphql-eslint", "unicorn"],
extends: [
"react-app",
"eslint:recommended",
@@ -25,29 +49,50 @@ module.exports = {
"prettier/react",
],
rules: {
+ // imports
"import/first": 2,
"import/no-unresolved": 0,
"import/order": [
2,
{
- pathGroups: [
- "shared",
- "shared/**",
- "pages",
- "pages/**",
- "features",
- "features/**",
- "models",
- ].map((pattern) => ({
- pattern,
- group: "internal",
- position: "after",
- })),
+ pathGroups: ALLOWED_PATH_GROUPS,
// TODO: Добавить сортировку `import "./index.scss";` (располагать внизу)
+ // TODO: Добавить сортировку `import *** from "react"` (располагать вверху)
pathGroupsExcludedImportTypes: ["builtin"],
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
},
],
+ // TODO: specify message: ("Please use allowed public API (not private imports!)")
+ "no-restricted-imports": [2, { patterns: DENIED_PATH_GROUPS }],
+ // variables
+ "prefer-const": 2,
+ "no-var": 2,
+ // base
+ "camelcase": [1, { ignoreDestructuring: true, ignoreImports: true, properties: "never" }],
+ "no-else-return": 2,
+ "max-len": [1, { code: 120 }],
+ "dot-notation": 2,
+ "eol-last": 2,
+ // alert, console
+ "no-alert": 2,
+ "no-console": 0,
+ // equals
+ "eqeqeq": 1,
+ "no-eq-null": 2,
+ // function
+ "max-params": [1, 2],
+ "max-lines-per-function": [1, 48],
+ "arrow-parens": [2, "always"],
+ // plugin:unicorn
+ "unicorn/no-for-loop": 2,
+ "unicorn/no-abusive-eslint-disable": 2,
+ "unicorn/no-array-instanceof": 2,
+ "unicorn/no-zero-fractions": 2,
+ "unicorn/prefer-includes": 2,
+ "unicorn/prefer-text-content": 2,
+ "unicorn/import-index": 2,
+ "unicorn/throw-new-error": 2,
+ // plugin:graphql
"@graphql-eslint/no-anonymous-operations": 2,
},
overrides: [
@@ -57,6 +102,7 @@ module.exports = {
plugins: ["@graphql-eslint"],
rules: {
"prettier/prettier": 0,
+ "max-len": 0,
},
},
],
diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml
index 725f8ae..baa835d 100644
--- a/.github/workflows/prod.yml
+++ b/.github/workflows/prod.yml
@@ -1,7 +1,7 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
-name: Production CI/CD
+name: CI
on:
push:
diff --git a/README.md b/README.md
index 13e4529..34eb98f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,16 @@
# github-client
+![Version](https://img.shields.io/github/package-json/v/ani-team/github-client)
+![CI](https://github.com/niyazm524/github-client/workflows/CI/badge.svg?branch=master)
+![Website](https://img.shields.io/website?down_message=offline&up_message=online&url=https%3A%2F%2Fgithub-client.gq)
+[![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fani-team%2Fgithub-client&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
+![GitHub top language](https://img.shields.io/github/languages/top/niyazm524/github-client)
+![GitHub commit activity](https://img.shields.io/github/commit-activity/w/ani-team/github-client)
+
+> Powered by [Feature Driven Development](https://www.notion.so/Feature-Driven-Development-dfe306d664ae4780bcf999ccdd15e532 "Entire app was designed and builded with FDD core conceptions")
+
GitHub client within the *React Akvelon 2020* course.
[wiki]: https://github.com/martis-git/github-client/wiki
@@ -22,16 +31,18 @@ GitHub client within the *React Akvelon 2020* course.
- See repo/collabs list and details of [any user](https://github-client.gq/gaearon)
-- See base info of [any public repository](https://github-client.gq/facebook/react) with [branches base manipulating](https://github-client.gq/facebook/react/tree/17.0.0-dev)
+- See base info and stats of [any public repository](https://github-client.gq/facebook/react) with [branches base manipulating](https://github-client.gq/facebook/react/tree/17.0.0-dev)
- Use search by [repositories](https://github-client.gq/search?o=desc&q=react&s=stars)/[users](https://github-client.gq/search?o=desc&q=google&s=repositories&type=users) with sorting
-- View corresponding page on github through origin button!
+- Try our end-to-end routing with Github with origin button, and specific adaptations on every page!
- Connect with your account safely - [by Github OAuth](https://github-client.gq/auth)
- [Get feedback](https://github-client.gq/some-unexisting-route-but-we-have-error-parking-page) if some errors occurred
+- Try our base interactivity on [UserPage](https://github-client.gq/gaearon) - following, starring
+- Get the best UX with our **loading && placeholder view logic**
- Try [**github-client right now**](https://github-client.gq) or [last dev version (but unstable)](https://dev.github-client.gq) =}
> If you found issues or have ideas for service - please, [share with us](https://github.com/ani-team/github-client/issues/new) 🔥
-
+![screen](docs/search.png)
## Technology stack
- **UI**: `react`, `antd`, `classnames`, `tailwindcss`
@@ -39,9 +50,9 @@ GitHub client within the *React Akvelon 2020* course.
- **Fetching**: `graphql`, `apollo-client (3+)`
- **API Codegen**: `graphql-codegen`
- **Routing**: `react-router`
-- Tests: `eslint`, `prettier`, `graphql-eslint`, `stylelint`
+- **Tests**: `eslint`, `prettier`, `graphql-eslint`, `stylelint`
- **Auth**: `GitHub OAuth`, `firebase`
-- CI/CD: `github-actions`, `firebase`
+- **CI/CD**: `github-actions`, `firebase`
diff --git a/docs/search.png b/docs/search.png
index 7254335..aad6984 100644
Binary files a/docs/search.png and b/docs/search.png differ
diff --git a/package-lock.json b/package-lock.json
index 7699a8c..f0cd8ca 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "github-client",
- "version": "0.1.0",
+ "version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6383,6 +6383,15 @@
}
}
},
+ "clean-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+ "integrity": "sha1-jffHquUf02h06PjQW5GAvBGj/tc=",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -8317,6 +8326,16 @@
}
}
},
+ "eslint-ast-utils": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz",
+ "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==",
+ "dev": true,
+ "requires": {
+ "lodash.get": "^4.4.2",
+ "lodash.zip": "^4.2.0"
+ }
+ },
"eslint-config-prettier": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.13.0.tgz",
@@ -8662,6 +8681,133 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz",
"integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA=="
},
+ "eslint-plugin-unicorn": {
+ "version": "23.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-23.0.0.tgz",
+ "integrity": "sha512-Vabo3cjl6cjyhcf+76CdQEY6suOFzK0Xh3xo0uL9VDYrDJP5+B6PjV0tHTYm82WZmFWniugFJM3ywHSNYTi/ZQ==",
+ "dev": true,
+ "requires": {
+ "ci-info": "^2.0.0",
+ "clean-regexp": "^1.0.0",
+ "eslint-ast-utils": "^1.1.0",
+ "eslint-template-visitor": "^2.2.1",
+ "eslint-utils": "^2.1.0",
+ "import-modules": "^2.0.0",
+ "lodash": "^4.17.20",
+ "pluralize": "^8.0.0",
+ "read-pkg-up": "^7.0.1",
+ "regexp-tree": "^0.1.21",
+ "reserved-words": "^0.1.2",
+ "safe-regex": "^2.1.1",
+ "semver": "^7.3.2"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "parse-json": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz",
+ "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true
+ },
+ "read-pkg": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+ "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+ "dev": true,
+ "requires": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+ "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+ "dev": true
+ }
+ }
+ },
+ "read-pkg-up": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+ "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+ "dev": true,
+ "requires": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ }
+ },
+ "safe-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
+ "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
+ "dev": true,
+ "requires": {
+ "regexp-tree": "~0.1.1"
+ }
+ },
+ "semver": {
+ "version": "7.3.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
+ "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ }
+ }
+ },
"eslint-scope": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
@@ -8671,6 +8817,18 @@
"estraverse": "^4.1.1"
}
},
+ "eslint-template-visitor": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-template-visitor/-/eslint-template-visitor-2.2.1.tgz",
+ "integrity": "sha512-q3SxoBXz0XjPGkUpwGVAwIwIPIxzCAJX1uwfVc8tW3v7u/zS7WXNH3I2Mu2MDz2NgSITAyKLRaQFPHu/iyKxDQ==",
+ "dev": true,
+ "requires": {
+ "babel-eslint": "^10.1.0",
+ "eslint-visitor-keys": "^1.3.0",
+ "esquery": "^1.3.1",
+ "multimap": "^1.1.0"
+ }
+ },
"eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
@@ -10815,6 +10973,12 @@
"resolve-cwd": "^2.0.0"
}
},
+ "import-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.0.0.tgz",
+ "integrity": "sha512-iczM/v9drffdNnABOKwj0f9G3cFDon99VcG1mxeBsdqnbd+vnQ5c2uAiCHNQITqFTOPaEvwg3VjoWCur0uHLEw==",
+ "dev": true
+ },
"imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -13133,6 +13297,12 @@
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
},
+ "lodash.zip": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz",
+ "integrity": "sha1-7GZi5IlkCO1KtsVCo5kLcswIACA=",
+ "dev": true
+ },
"log-symbols": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
@@ -13281,8 +13451,7 @@
"longest-streak": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz",
- "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==",
- "dev": true
+ "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="
},
"loose-envify": {
"version": "1.4.0",
@@ -13407,7 +13576,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
"integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==",
- "dev": true,
"requires": {
"repeat-string": "^1.0.0"
}
@@ -13463,6 +13631,67 @@
"parse-entities": "^2.0.0"
}
},
+ "mdast-util-gfm": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.0.tgz",
+ "integrity": "sha512-HLfygQL6HdhJhFbLta4Ki9hClrzyAxRjyRvpm5caN65QZL+NyHPmqFlnF9vm1Rn58JT2+AbLwNcEDY4MEvkk8Q==",
+ "requires": {
+ "mdast-util-gfm-autolink-literal": "^0.1.0",
+ "mdast-util-gfm-strikethrough": "^0.2.0",
+ "mdast-util-gfm-table": "^0.1.0",
+ "mdast-util-gfm-task-list-item": "^0.1.0"
+ }
+ },
+ "mdast-util-gfm-autolink-literal": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.1.tgz",
+ "integrity": "sha512-gJ2xSpqKCetSr22GEWpZH3f5ffb4pPn/72m4piY0v7T/S+O7n7rw+sfoPLhb2b4O7WdnERoYdALRcmD68FMtlw=="
+ },
+ "mdast-util-gfm-strikethrough": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.2.tgz",
+ "integrity": "sha512-T37ZbaokJcRbHROXmoVAieWnesPD5N21tv2ifYzaGRLbkh1gknItUGhZzHefUn5Zc/eaO/iTDSAFOBrn/E8kWw==",
+ "requires": {
+ "mdast-util-to-markdown": "^0.5.0"
+ }
+ },
+ "mdast-util-gfm-table": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.4.tgz",
+ "integrity": "sha512-T4xFSON9kUb/IpYA5N+KGWcsdGczAvILvKiXQwUGind6V9fvjPCR9yhZnIeaLdBWXaz3m/Gq77ZtuLMjtFR4IQ==",
+ "requires": {
+ "markdown-table": "^2.0.0",
+ "mdast-util-to-markdown": "^0.5.0"
+ }
+ },
+ "mdast-util-gfm-task-list-item": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.5.tgz",
+ "integrity": "sha512-6O0bt34r+e7kYjeSwedhjDPYraspKIYKbhvhQEEioL7gSmXDxhN7WQW2KoxhVMpNzjNc03yC7K5KH6NHlz2jOA==",
+ "requires": {
+ "mdast-util-to-markdown": "^0.5.0"
+ }
+ },
+ "mdast-util-to-markdown": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.5.4.tgz",
+ "integrity": "sha512-0jQTkbWYx0HdEA/h++7faebJWr5JyBoBeiRf0u3F4F3QtnyyGaWIsOwo749kRb1ttKrLLr+wRtOkfou9yB0p6A==",
+ "requires": {
+ "@types/unist": "^2.0.0",
+ "longest-streak": "^2.0.0",
+ "mdast-util-to-string": "^2.0.0",
+ "parse-entities": "^2.0.0",
+ "repeat-string": "^1.0.0",
+ "zwitch": "^1.0.0"
+ },
+ "dependencies": {
+ "mdast-util-to-string": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz",
+ "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="
+ }
+ }
+ },
"mdast-util-to-string": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz",
@@ -13678,6 +13907,56 @@
"parse-entities": "^2.0.0"
}
},
+ "micromark-extension-gfm": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.1.tgz",
+ "integrity": "sha512-lJlhcOqzoJdjQg+LMumVHdUQ61LjtqGdmZtrAdfvatRUnJTqZlRwXXHdLQgNDYlFw4mycZ4NSTKlya5QcQXl1A==",
+ "requires": {
+ "micromark": "~2.10.0",
+ "micromark-extension-gfm-autolink-literal": "~0.5.0",
+ "micromark-extension-gfm-strikethrough": "~0.6.0",
+ "micromark-extension-gfm-table": "~0.4.0",
+ "micromark-extension-gfm-tagfilter": "~0.3.0",
+ "micromark-extension-gfm-task-list-item": "~0.3.0"
+ }
+ },
+ "micromark-extension-gfm-autolink-literal": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.1.tgz",
+ "integrity": "sha512-j30923tDp0faCNDjwqe4cMi+slegbGfc3VEAExEU8d54Q/F6pR6YxCVH+6xV0ItRoj3lCn1XkUWcy6FC3S9BOw==",
+ "requires": {
+ "micromark": "~2.10.0"
+ }
+ },
+ "micromark-extension-gfm-strikethrough": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.2.tgz",
+ "integrity": "sha512-aehEEqtTn3JekJNwZZxa7ZJVfzmuaWp4ew6x6sl3VAKIwdDZdqYeYSQIrNKwNgH7hX0g56fAwnSDLusJggjlCQ==",
+ "requires": {
+ "micromark": "~2.10.0"
+ }
+ },
+ "micromark-extension-gfm-table": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.1.tgz",
+ "integrity": "sha512-xVpqOnfFaa2OtC/Y7rlt4tdVFlUHdoLH3RXAZgb/KP3DDyKsAOx6BRS3UxiiyvmD/p2l6VUpD4bMIniuP4o4JA==",
+ "requires": {
+ "micromark": "~2.10.0"
+ }
+ },
+ "micromark-extension-gfm-tagfilter": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz",
+ "integrity": "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q=="
+ },
+ "micromark-extension-gfm-task-list-item": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.2.tgz",
+ "integrity": "sha512-cm8lYS10YAqeXE9B27TK3u1Ihumo3H9p/3XumT+jp8vSuSbSpFIJe0bDi2kq4YAAIxtcTzUOxhEH4ko2/NYDkQ==",
+ "requires": {
+ "micromark": "~2.10.0"
+ }
+ },
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
@@ -13967,6 +14246,12 @@
"resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz",
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE="
},
+ "multimap": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/multimap/-/multimap-1.1.0.tgz",
+ "integrity": "sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==",
+ "dev": true
+ },
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@@ -15087,6 +15372,12 @@
"semver-compare": "^1.0.0"
}
},
+ "pluralize": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+ "dev": true
+ },
"pn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz",
@@ -17587,6 +17878,12 @@
"resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz",
"integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA=="
},
+ "regexp-tree": {
+ "version": "0.1.21",
+ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.21.tgz",
+ "integrity": "sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==",
+ "dev": true
+ },
"regexp.prototype.flags": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
@@ -17867,6 +18164,15 @@
"unified": "^9.0.0"
}
},
+ "remark-gfm": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz",
+ "integrity": "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==",
+ "requires": {
+ "mdast-util-gfm": "^0.1.0",
+ "micromark-extension-gfm": "^0.3.0"
+ }
+ },
"remark-parse": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-8.0.3.tgz",
@@ -18071,6 +18377,12 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
+ "reserved-words": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz",
+ "integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=",
+ "dev": true
+ },
"resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
@@ -22196,6 +22508,11 @@
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz",
"integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ=="
+ },
+ "zwitch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz",
+ "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="
}
}
}
diff --git a/package.json b/package.json
index b784946..3d6994b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "github-client",
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^4.2.2",
@@ -28,6 +28,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"react-syntax-highlighter": "^15.3.1",
+ "remark-gfm": "^1.0.0",
"tailwindcss": "^1.9.5",
"typescript": "^3.7.5",
"use-query-params": "^1.1.9"
@@ -76,6 +77,7 @@
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-plugin-prettier": "^3.1.4",
+ "eslint-plugin-unicorn": "^23.0.0",
"husky": "^4.3.0",
"lint-staged": "^10.4.2",
"prettier": "^2.1.2",
diff --git a/src/.deploy/index.ts b/src/.deploy/index.ts
index 4f3eedb..8443585 100644
--- a/src/.deploy/index.ts
+++ b/src/.deploy/index.ts
@@ -1 +1,9 @@
+/**
+ * Модуль для облегченной реализации для temp-стендов на firebase (guest mode)
+ * @remark
+ * - Связано с тем, что сложно реализовать полноценную авторизацию для temp-стендов, разворачиваемых
+ * Во время pull-requests
+ * - Модуль нужен только для разработки - во время основной работы приложения же - не используется
+ */
+
export * from "./temp-stand";
diff --git a/src/.deploy/temp-stand.ts b/src/.deploy/temp-stand.ts
index 2dfa001..a19bdd6 100644
--- a/src/.deploy/temp-stand.ts
+++ b/src/.deploy/temp-stand.ts
@@ -1,5 +1,6 @@
-import { CREDENTIAL_KEY } from "features/auth";
+import { Auth } from "features";
+const { CREDENTIAL_KEY } = Auth;
const tempStandRegex = /^(github-client-47c49|dev-github-client)--pr\d+.+\.web\.app$/;
const isTempStand = () => tempStandRegex.test(window.location.host);
@@ -7,6 +8,9 @@ const isTempStand = () => tempStandRegex.test(window.location.host);
const STAND_URL_ENV = "REACT_APP_DEV_STORAGE_URL";
const devStorageUrl = process.env[STAND_URL_ENV];
+/**
+ * Инициализация гостевого режима с псевдо-авторизацией
+ */
export const loadLocalStorageFromDevIfNeeded = async () => {
if (!isTempStand() || !devStorageUrl) {
if(!devStorageUrl) {
diff --git a/src/app/error-handling/error-catcher.tsx b/src/app/error-handling/error-catcher.tsx
deleted file mode 100644
index b3880d0..0000000
--- a/src/app/error-handling/error-catcher.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React, { useEffect, useState, ReactNode } from "react";
-import { ServerError, useApolloClient } from "@apollo/client";
-import { onError } from "@apollo/client/link/error";
-import { GraphQLError } from "graphql";
-import { useLocation } from "react-router";
-import { AppError } from "models";
-import { ErrorDefinitions } from "./error-definitions";
-
-const isGithubError = (error: any): error is { type: string } => {
- return typeof error.type === "string";
-};
-
-const isServerError = (error: any): error is ServerError => {
- return typeof error.statusCode === "number";
-};
-
-function mapError(error: GraphQLError | Error | ServerError | undefined): AppError | null {
- if (!error) return null;
- if (isGithubError(error)) {
- // FIXME: handle 403 and 500 errors as well w/o side effects
- if (error.type === "NOT_FOUND") {
- return ErrorDefinitions[error.type];
- }
- }
- if (isServerError(error)) {
- if (error.statusCode === 401) return ErrorDefinitions.UNAUTHORIZED;
- }
- // TODO: handle network errors and whatever can be broken
- return null;
-}
-
-type Props = PropsWithChildren<{
- handler: (props: { error: AppError }) => ReactNode;
-}>;
-
-export default function ErrorCatcher({ handler, children }: Props) {
- const apolloClient = useApolloClient();
- const location = useLocation();
- const [error, setError] = useState(null);
-
- useEffect(() => {
- const errorLink = onError(({ graphQLErrors, networkError }) => {
- setError(mapError(graphQLErrors?.[0] || networkError));
- });
- apolloClient.setLink(errorLink.concat(apolloClient.link));
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- useEffect(() => setError(null), [location]);
-
- if (error) {
- return <>{handler({ error })}>;
- }
- return <>{children}>;
-}
diff --git a/src/app/error-handling/index.ts b/src/app/error-handling/index.ts
deleted file mode 100644
index 6bf79c6..0000000
--- a/src/app/error-handling/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as ErrorCatcher } from "./error-catcher";
-export { ErrorDefinitions } from "./error-definitions";
diff --git a/src/app/header/hooks.ts b/src/app/header/hooks.ts
new file mode 100644
index 0000000..74d9d27
--- /dev/null
+++ b/src/app/header/hooks.ts
@@ -0,0 +1,39 @@
+import { KeyboardEventHandler } from "react";
+import { StringParam, useQueryParams } from "use-query-params";
+import { useHistory, useLocation } from "react-router-dom";
+import * as qs from "query-string";
+
+// FIXME: get from `pages`?
+const SEARCH_URL = "/search";
+
+/**
+ * @hook Логика обработки инпута поиска
+ */
+export const useSearchInput = () => {
+ // !!! FIXME: limit scope of query-params literals
+ const [query] = useQueryParams({
+ q: StringParam,
+ type: StringParam,
+ s: StringParam,
+ o: StringParam,
+ });
+ const location = useLocation();
+ const history = useHistory();
+
+ /**
+ * Обработка инпута поиска
+ */
+ const handleKeyDown: KeyboardEventHandler = ({ key, currentTarget }) => {
+ if (key === "Enter" && currentTarget.value) {
+ const q = currentTarget.value;
+ history.push(`${SEARCH_URL}?${qs.stringify({ ...query, q })}`);
+ }
+ };
+ /**
+ * Поисковой запрос
+ * @remark Если не страница поиска - обнуляем инпут
+ */
+ const searchValue = location.pathname === SEARCH_URL ? query.q ?? "" : "";
+
+ return { handleKeyDown, searchValue };
+};
diff --git a/src/app/header/index.tsx b/src/app/header/index.tsx
index 1dc0533..df42a15 100644
--- a/src/app/header/index.tsx
+++ b/src/app/header/index.tsx
@@ -1,26 +1,19 @@
import React from "react";
import { Layout, Input } from "antd";
-import { Link, useHistory, useLocation } from "react-router-dom";
-import { StringParam, useQueryParams } from "use-query-params";
-import * as qs from "query-string";
+import { Link } from "react-router-dom";
+import { GITHUB_MAIN, GITHUB_FEEDBACK } from "shared/get-env";
import { Auth } from "features";
import { ReactComponent as IcLogo } from "./logo.svg";
+import { useSearchInput } from "./hooks";
import "./index.scss";
-const FEEDBACK_URL = "https://github.com/ani-team/github-client/issues/new";
-const GITHUB_URL = "https://github.com/ani-team/github-client";
-
+/**
+ * Хедер приложения
+ * @remark Содержит поисковой инпут с базовой логикой
+ */
const Header = () => {
const { isAuth } = Auth.useAuth();
- // !!! FIXME: limit scope of query-params literals
- const [query] = useQueryParams({
- q: StringParam,
- type: StringParam,
- s: StringParam,
- o: StringParam,
- });
- const location = useLocation();
- const history = useHistory();
+ const { handleKeyDown, searchValue } = useSearchInput();
return (
@@ -33,25 +26,24 @@ const Header = () => {
{
- if (key === "Enter" && currentTarget.value) {
- history.push(
- `/search?${qs.stringify({
- q: currentTarget.value,
- type: query.type,
- s: query.s,
- o: query.o,
- })}`,
- );
- }
- }}
+ defaultValue={searchValue}
+ onKeyDown={handleKeyDown}
/>
)}
-
+
GitHub
-
+
Feedback
diff --git a/src/app/hocs/index.ts b/src/app/hocs/index.ts
index dd24d11..11c1626 100644
--- a/src/app/hocs/index.ts
+++ b/src/app/hocs/index.ts
@@ -1,5 +1,15 @@
+import { compose } from "shared/helpers";
import withApollo from "./with-apollo";
import withRouter from "./with-router";
+import withAntd from "./with-antd";
-// Потом какой-нибудь `compose` метод заинсталлим откуда-нить и покрасивше будет
-export const withHocs = (component: () => JSX.Element) => withRouter(withApollo(component));
+/**
+ * @hoc Инициализирующая логика приложения
+ * @remark Содержит:
+ * - логику инициализации antd (withAntd)
+ * - логику подключения к API (withApollo)
+ * - логику инициализации роутера (withRouter)
+ */
+export const withHocs = compose(withAntd, withRouter, withApollo);
+
+export { ErrorHandlingProvider } from "./with-error-handling";
diff --git a/src/app/hocs/with-antd.tsx b/src/app/hocs/with-antd.tsx
new file mode 100644
index 0000000..c6549e9
--- /dev/null
+++ b/src/app/hocs/with-antd.tsx
@@ -0,0 +1,18 @@
+import React from "react";
+import { ConfigProvider } from "antd";
+
+// eslint-disable-next-line max-len
+// https://ant.design/docs/react/faq#How-do-I-prevent-Select-Dropdown-DatePicker-TimePicker-Popover-Popconfirm-scrolling-with-the-page
+
+/**
+ * @hoc Инициализация antd для корректного использования компонентов
+ * @remark Для попапов в системе - возвращаем parentElement (для фикса бага) или document.body (defaultValue)
+ * @see https://github.com/ani-team/github-client/issues/162#issuecomment-736676022
+ */
+const withAntd = (component: Component) => () => (
+ parentElement || document.body}>
+ {component()}
+
+);
+
+export default withAntd;
diff --git a/src/app/hocs/with-apollo.tsx b/src/app/hocs/with-apollo.tsx
index df14ec9..4bfecb0 100644
--- a/src/app/hocs/with-apollo.tsx
+++ b/src/app/hocs/with-apollo.tsx
@@ -4,12 +4,17 @@ import { setContext } from "@apollo/client/link/context";
import { API_URL } from "shared/get-env";
import { Auth } from "features";
-// TODO: Уточнить, нужно ли дополнительно задавать контекст
-
+/**
+ * Инициализация API.baseUrl
+ */
const httpLink = createHttpLink({
uri: API_URL,
});
+/**
+ * Логика авторизации
+ * FIXME: вынести в features/auth?
+ */
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = Auth.getToken();
@@ -23,6 +28,7 @@ const authLink = setContext((_, { headers }) => {
});
/**
+ * Инициализация инстанса клиента
* @see https://www.apollographql.com/docs/react/networking/authentication/
*/
const client = new ApolloClient({
@@ -32,9 +38,9 @@ const client = new ApolloClient({
});
/**
- * Обертка для подключения и работы с API
+ * @hoc Инициализация подключения apollo для работы с API
*/
-const withApollo = (component: () => JSX.Element) => () => (
+const withApollo = (component: Component) => () => (
{component()}
);
diff --git a/src/app/hocs/with-error-handling.tsx b/src/app/hocs/with-error-handling.tsx
new file mode 100644
index 0000000..e4b989e
--- /dev/null
+++ b/src/app/hocs/with-error-handling.tsx
@@ -0,0 +1,21 @@
+import React, { lazy } from "react";
+import { alert } from "shared/helpers";
+import { Error } from "features";
+import { AppError } from "models";
+
+const ErrorPage = lazy(() => import("pages/error"));
+
+/**
+ * @hoc Инициализация отлова ошибок
+ */
+export const ErrorHandlingProvider = ({ children }: PropsWithChildren) => {
+ const onNetworkError = ({ message, description }: AppError) => alert.warn(message, description);
+ return (
+ }
+ >
+ {children}
+
+ );
+};
diff --git a/src/app/hocs/with-router.tsx b/src/app/hocs/with-router.tsx
index f660860..0f7bdbb 100644
--- a/src/app/hocs/with-router.tsx
+++ b/src/app/hocs/with-router.tsx
@@ -4,9 +4,9 @@ import { BrowserRouter, Route } from "react-router-dom";
import { QueryParamProvider } from "use-query-params";
/**
- * Инициализация роутера с провайдером для работы с get-параметрами
+ * @hoc Инициализация роутера с провайдером для работы с get-параметрами
*/
-const withRouter = (component: () => JSX.Element) => () => (
+const withRouter = (component: Component) => () => (
}>
{component()}
diff --git a/src/app/index.scss b/src/app/index.scss
index 5bd1364..06d215c 100644
--- a/src/app/index.scss
+++ b/src/app/index.scss
@@ -1,15 +1,17 @@
// NOTE: Временно используем вспомогательные utilities-классы, на время разработки прототипа приложения
@import "~tailwindcss/dist/utilities.css";
-@import "./vars.scss";
-@import "./normalize.scss";
-@import "./normalize-antd.scss";
-@import "./utils.scss";
+// css-переменные проекта
+@import "./styles/vars.scss";
+// нормализация стилей проекта
+@import "./styles/normalize.scss";
+// нормализация стилей antd компонентов
+@import "./styles/normalize-antd.scss";
+// классы-утилиты
+@import "./styles/utils.scss";
.gc-app {
display: flex;
flex-direction: column;
- // !!! FIXME: temp (caus #86)
- overflow: overlay;
font-family: var(--ff-primary);
background-color: var(--clr-background);
@@ -23,5 +25,10 @@
.ant-layout {
background-color: unset;
+
+ &-content {
+ // FIXME: deprecated (find better solution)
+ overflow: overlay;
+ }
}
}
diff --git a/src/app/index.tsx b/src/app/index.tsx
index 1e23703..30a6f02 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -1,16 +1,14 @@
-import React, { lazy } from "react";
+import React from "react";
import { Layout } from "antd";
import Routing from "pages";
-import { ErrorCatcher } from "./error-handling";
import Header from "./header";
-import { withHocs } from "./hocs";
+import { withHocs, ErrorHandlingProvider } from "./hocs";
import "./index.scss";
-// !!! FIXME: manage access
-const ErrorPage = lazy(() => import("pages/error"));
/**
* Entry-point приложения
- * @remark Содержит в HOC-обертке логику подключения к API (apollo)
+ * @remark Содержит в HOC-обертке инициализирующую логику приложения
+ * @see withHocs
*/
const App = () => {
return (
@@ -18,9 +16,9 @@ const App = () => {
- }>
+
-
+
diff --git a/src/app/normalize-antd.scss b/src/app/styles/normalize-antd.scss
similarity index 100%
rename from src/app/normalize-antd.scss
rename to src/app/styles/normalize-antd.scss
diff --git a/src/app/normalize.scss b/src/app/styles/normalize.scss
similarity index 100%
rename from src/app/normalize.scss
rename to src/app/styles/normalize.scss
diff --git a/src/app/utils.scss b/src/app/styles/utils.scss
similarity index 100%
rename from src/app/utils.scss
rename to src/app/styles/utils.scss
diff --git a/src/app/vars.scss b/src/app/styles/vars.scss
similarity index 100%
rename from src/app/vars.scss
rename to src/app/styles/vars.scss
diff --git a/src/features/auth/consts.ts b/src/features/auth/consts.ts
index c160e28..ce505ce 100644
--- a/src/features/auth/consts.ts
+++ b/src/features/auth/consts.ts
@@ -3,12 +3,15 @@ import { UserCredential } from "./types";
/** @localStorage Учетные данные */
export const CREDENTIAL_KEY = "GITHUB-CLIENT__CREDENTIAL";
+/** @localStorage Получение учетных данных */
export const getCredential = () => {
return JSON.parse(localStorage.getItem(CREDENTIAL_KEY) || "") as UserCredential;
};
+/** @localStorage Получение токена доступа */
export const getToken = () => getCredential().accessToken;
+/** Базовые роуты модуля авторизации */
export const routes = {
main: "/",
logout: "/",
diff --git a/src/features/auth/firebase/auth-github.ts b/src/features/auth/firebase/auth-github.ts
index a275f56..13e85fb 100644
--- a/src/features/auth/firebase/auth-github.ts
+++ b/src/features/auth/firebase/auth-github.ts
@@ -1,6 +1,10 @@
import { AuthContext } from "../types";
import firebase from "./init";
+/**
+ * Проверка респонса от сервиса во время авторизации
+ * @remark Минимально нужные поля для продолжения работы в системе
+ */
const isValidAuthContext = (ctx: any): ctx is AuthContext => {
return (
typeof ctx?.credential?.accessToken === "string" &&
@@ -8,7 +12,11 @@ const isValidAuthContext = (ctx: any): ctx is AuthContext => {
);
};
-export default function authGithub() {
+/**
+ * Авторизация Github OAuth через firebase
+ * @remark Стоит использовать только на странице Авторизации!!!
+ */
+function authGithub() {
const provider = new firebase.auth.GithubAuthProvider();
provider.addScope("repo");
provider.addScope("user:follow");
@@ -29,3 +37,5 @@ export default function authGithub() {
};
});
}
+
+export default authGithub;
diff --git a/src/features/auth/firebase/init.ts b/src/features/auth/firebase/init.ts
index 4cf1ac0..146ca42 100644
--- a/src/features/auth/firebase/init.ts
+++ b/src/features/auth/firebase/init.ts
@@ -2,6 +2,9 @@ import firebase from "firebase/app";
import "firebase/auth";
import { firebaseConfig } from "shared/get-env";
+/**
+ * Инициализация firebase приложения
+ */
firebase.initializeApp(firebaseConfig);
export default firebase;
diff --git a/src/features/auth/hooks.ts b/src/features/auth/hooks.ts
index ae0197e..3cf1a8d 100644
--- a/src/features/auth/hooks.ts
+++ b/src/features/auth/hooks.ts
@@ -2,6 +2,9 @@ import { useLocalStorage } from "shared/hooks";
import { CREDENTIAL_KEY } from "./consts";
import { UserCredential } from "./types";
+/**
+ * @hook Использование контекста авторизации и соответствующих методов
+ */
export const useAuth = () => {
const [viewer, setViewer] = useLocalStorage(CREDENTIAL_KEY, null);
const isAuth = !!viewer;
diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts
index 2b5a032..b88fd80 100644
--- a/src/features/auth/index.ts
+++ b/src/features/auth/index.ts
@@ -1,5 +1,4 @@
-export { default as Page } from "./page";
export { default as User } from "./user";
export * from "./consts";
-// FIXME: temp?
export * from "./hooks";
+export * from "./firebase";
diff --git a/src/features/auth/types.ts b/src/features/auth/types.ts
index 1602490..ce697b8 100644
--- a/src/features/auth/types.ts
+++ b/src/features/auth/types.ts
@@ -4,10 +4,18 @@ export type CredentialWithToken = {
};
};
+/**
+ * Контекст авторизации
+ */
export type AuthContext = import("firebase").default.auth.UserCredential & CredentialWithToken;
+/**
+ * Учетные данные пользователя
+ */
export type UserCredential = {
+ /** Токен доступа */
accessToken: string;
+ /** Логин пользователя */
username: string;
// NOTE: Возможно список хранимых полей будет расширяться позднее
};
diff --git a/src/features/auth/user/index.tsx b/src/features/auth/user/index.tsx
index a93da94..5a0cead 100644
--- a/src/features/auth/user/index.tsx
+++ b/src/features/auth/user/index.tsx
@@ -1,8 +1,12 @@
import React from "react";
import { Button } from "antd";
+import { Link } from "react-router-dom";
import { useAuth } from "../hooks";
import { routes } from "../consts";
+/**
+ * Плашка пользователя с базовой информацией
+ */
const User = () => {
const { isAuth, logout, viewer } = useAuth();
@@ -15,9 +19,9 @@ const User = () => {
{isAuth && (
<>
{/* FIXME: use h3 instead */}
-
+
{viewer?.username}
-
+
diff --git a/src/features/error/catcher.tsx b/src/features/error/catcher.tsx
new file mode 100644
index 0000000..0748b1e
--- /dev/null
+++ b/src/features/error/catcher.tsx
@@ -0,0 +1,54 @@
+import React, { useEffect, useState, ReactNode } from "react";
+import { onError } from "@apollo/client/link/error";
+import { useApolloClient } from "@apollo/client";
+import { useLocation } from "react-router";
+import { AppError } from "models";
+import { Definitions } from "./definitions";
+import { mapError } from "./helpers";
+
+type Props = PropsWithChildren<{
+ /** Отрисовщик-обработчик ошибки */
+ handler: (props: { error: AppError }) => ReactNode;
+ onNetworkError?: (error: AppError) => void;
+}>;
+
+/**
+ * @hook Логика обработки и хранения ошибок
+ */
+const useAppError = () => {
+ const apolloClient = useApolloClient();
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const errorLink = onError(({ graphQLErrors, networkError }) => {
+ const appError = mapError(graphQLErrors?.[0] || networkError);
+ setError(appError || null);
+ });
+ apolloClient.setLink(errorLink.concat(apolloClient.link));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return { error, setError };
+};
+
+/**
+ * @feature Обертка для обработки ошибок
+ * FIXME: add ErrorBoundaries
+ */
+const Catcher = ({ handler, children, onNetworkError }: Props) => {
+ const location = useLocation();
+ const { error, setError } = useAppError();
+
+ useEffect(() => setError(null), [location, setError]);
+ useEffect(() => {
+ if (error?.code !== Definitions.NETWORK_ERROR.code) return;
+ onNetworkError?.(error);
+ }, [error, onNetworkError]);
+
+ if (error && error.code !== Definitions.NETWORK_ERROR.code) {
+ return <>{handler({ error })}>;
+ }
+ return <>{children}>;
+};
+
+export default Catcher;
diff --git a/src/app/error-handling/error-definitions.ts b/src/features/error/definitions.ts
similarity index 70%
rename from src/app/error-handling/error-definitions.ts
rename to src/features/error/definitions.ts
index b11d3fc..8051907 100644
--- a/src/app/error-handling/error-definitions.ts
+++ b/src/features/error/definitions.ts
@@ -1,6 +1,9 @@
import { AppError } from "models";
-export const ErrorDefinitions: Record = {
+/**
+ * Обрабатываемые ошибки приложения
+ */
+export const Definitions: Record = {
UNAUTHORIZED: {
code: 401,
message: "You shall not pass!",
@@ -21,4 +24,9 @@ export const ErrorDefinitions: Record = {
message: "500 Server Error\nSomething’s wrong!",
description: "we’re doing smth",
},
+ NETWORK_ERROR: {
+ code: -1,
+ message: "Check your connection",
+ description: "Failed to make a request",
+ },
};
diff --git a/src/features/error/helpers.ts b/src/features/error/helpers.ts
new file mode 100644
index 0000000..0778120
--- /dev/null
+++ b/src/features/error/helpers.ts
@@ -0,0 +1,40 @@
+import { ServerError } from "@apollo/client";
+import { GraphQLError } from "graphql";
+import { AppError } from "models";
+import { Definitions } from "./definitions";
+
+/**
+ * Проверка: является ли ошибка GitHub Error
+ */
+const isGithubError = (error: any): error is { type: string } => {
+ return typeof error.type === "string";
+};
+
+/**
+ * Проверка: является ли ошибка серверной
+ */
+const isServerError = (error: any): error is ServerError => {
+ return typeof error.statusCode === "number";
+};
+
+/**
+ * Соответствие полученной ошибки с прописанными и обрабатываемыми на уровне приложения
+ * @see ErrorDefinitions
+ */
+export function mapError(error?: GraphQLError | Error | ServerError): AppError | null {
+ if (!error) return null;
+ if (isGithubError(error)) {
+ // FIXME: handle 403 and 500 errors as well w/o side effects
+ if (error.type === "NOT_FOUND") {
+ return Definitions[error.type];
+ }
+ }
+ if (isServerError(error)) {
+ if (error.statusCode === 401) return Definitions.UNAUTHORIZED;
+ }
+ if (error instanceof TypeError && error.message === "Failed to fetch") {
+ return Definitions.NETWORK_ERROR;
+ }
+ // TODO: handle other errors and whatever can be broken
+ return null;
+}
diff --git a/src/features/error/index.ts b/src/features/error/index.ts
new file mode 100644
index 0000000..be66f68
--- /dev/null
+++ b/src/features/error/index.ts
@@ -0,0 +1,2 @@
+export { default as Catcher } from "./catcher";
+export * from "./definitions";
diff --git a/src/features/hero-sheet/index.tsx b/src/features/hero-sheet/index.tsx
index e1236fa..bf28ee1 100644
--- a/src/features/hero-sheet/index.tsx
+++ b/src/features/hero-sheet/index.tsx
@@ -5,19 +5,35 @@ import { ReactComponent as Icon } from "./assets/github-icon.svg";
import { ReactComponent as SadIcon } from "./assets/github-icon-sad.svg";
import "./index.scss";
+type UserAction = {
+ /** Текст действия */
+ text: string;
+ /** Обработчик действия */
+ to: () => void;
+};
+
type Props = {
+ /** Основной заголовок */
title: string;
+ /** Описание/примечание */
description: string;
- action?: { text: string; to: () => void };
+ /** Предлагаемое действие */
+ action?: UserAction;
+ /** Использовать `sad` аватар */
useSadHero?: boolean;
};
+/**
+ * Hero секция
+ * @remark Используется как базовое отображение информации для HomePage, ErrorPage
+ */
const HeroSheet = ({ title, description, action, useSadHero = false }: Props) => {
const history = useHistory();
- const preferredAction = action ?? {
+ const backAction: UserAction = {
text: "Back",
to: () => (history.length > 1 ? history.goBack() : history.push("/")),
};
+ const preferredAction = action ?? backAction;
const HeroIcon = useSadHero ? SadIcon : Icon;
return (
diff --git a/src/features/index.ts b/src/features/index.ts
index bc7c243..0e6b309 100644
--- a/src/features/index.ts
+++ b/src/features/index.ts
@@ -1,5 +1,6 @@
// Импортим отдельно, т.к. фича экспортит из себя много подмодулей
import * as Auth from "./auth";
+import * as Error from "./error";
// FIXME: Нормализовать экспорты к единому виду
@@ -12,3 +13,4 @@ export { default as Search } from "./search";
export { default as Origin } from "./origin";
export { default as HeroSheet } from "./hero-sheet";
export { Auth };
+export { Error };
diff --git a/src/features/repo-details/card-collaborators/index.tsx b/src/features/repo-details/card-collaborators/index.tsx
new file mode 100644
index 0000000..84ac0e0
--- /dev/null
+++ b/src/features/repo-details/card-collaborators/index.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { Link } from "react-router-dom";
+import DetailsCard from "../details-card";
+import { RepoDetailsQuery } from "../queries.gen";
+
+// FIXME: move to models
+type Collaborator = {
+ /** id */
+ id: string;
+ /** Имя пользователя */
+ name: string;
+ /** Логин пользователя */
+ login: string;
+ /** Аватар */
+ avatarUrl: string;
+};
+
+type Props = {
+ /** Данные по репозиторию */
+ repository: RepoDetailsQuery["repository"];
+};
+
+/**
+ * Коллабораторы репозитория
+ */
+const CardCollaborators = ({ repository }: Props) => {
+ const collaborators = repository?.collaborators?.nodes?.filter(
+ (collaborator): collaborator is Collaborator => !!collaborator,
+ );
+
+ if (!collaborators) return null;
+ return (
+
+ {collaborators?.map(({ id, login, avatarUrl }) => (
+
+
+
+ {login}
+
+
+ ))}
+
+ );
+};
+
+export default CardCollaborators;
diff --git a/src/features/repo-details/card-common/index.tsx b/src/features/repo-details/card-common/index.tsx
new file mode 100644
index 0000000..67cebf1
--- /dev/null
+++ b/src/features/repo-details/card-common/index.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import { Skeleton, Tag } from "antd";
+import { Language, RepoIdentity } from "models";
+import DetailsCard from "../details-card";
+import { RepoDetailsQuery } from "../queries.gen";
+
+type Props = {
+ /** Данные по репозиторию */
+ repository: RepoDetailsQuery["repository"];
+ /** repo identity */
+ identity: RepoIdentity;
+ /** Флаг загрузки */
+ loading: boolean;
+};
+
+/**
+ * Общая информация по репозиторию
+ */
+const CardCommon = (props: Props) => {
+ const { repository, identity, loading } = props;
+ const languages = repository?.languages?.nodes?.filter((lang): lang is Language => !!lang);
+ return (
+
+ {loading && (
+
+ )}
+ {repository?.description ? (
+ {repository?.description}
+ ) : (
+ No description, website, or topics provided.
+ )}
+
+ {repository?.homepageUrl && (
+
+ {repository.homepageUrl}
+
+ )}
+ {languages?.map(({ id, name, color }) => (
+
+ {name}
+
+ ))}
+
+ );
+};
+
+export default CardCommon;
diff --git a/src/features/repo-details/details-card/index.tsx b/src/features/repo-details/details-card/index.tsx
index 1595942..7786c3d 100644
--- a/src/features/repo-details/details-card/index.tsx
+++ b/src/features/repo-details/details-card/index.tsx
@@ -4,13 +4,13 @@ import "./index.scss";
type Props = PropsWithChildren<{ title: string; className?: string; primary?: boolean }>;
-function DetailsCard({ title, className, primary, children }: Props) {
+const DetailsCard = ({ title, className, primary, children }: Props) => {
return (
{title}
{children}
);
-}
+};
export default DetailsCard;
diff --git a/src/features/repo-details/index.tsx b/src/features/repo-details/index.tsx
index 7129b34..8e68602 100644
--- a/src/features/repo-details/index.tsx
+++ b/src/features/repo-details/index.tsx
@@ -1,25 +1,20 @@
-import { Skeleton, Tag, Alert } from "antd";
+import { Alert, Spin } from "antd";
import React from "react";
-import { Link } from "react-router-dom";
-import { Language, RepoIdentity } from "../../models";
-import DetailsCard from "./details-card";
+import { RepoIdentity } from "models";
+import CardCommon from "./card-common";
+import CardCollaborators from "./card-collaborators";
import { useRepoDetailsQuery } from "./queries.gen";
import "./index.scss";
-// !!! FIXME: decompose
-
type Props = {
+ /** repo identity */
repo: RepoIdentity;
};
-type Collaborator = {
- id: string;
- name: string;
- login: string;
- avatarUrl: string;
-};
-
-function RepoDetails({ repo: identity }: Props) {
+/**
+ * @feature Информация по репозиторию
+ */
+const RepoDetails = ({ repo: identity }: Props) => {
const { data, loading } = useRepoDetailsQuery({
variables: {
name: identity.name,
@@ -29,65 +24,20 @@ function RepoDetails({ repo: identity }: Props) {
});
const repository = data?.repository;
- const languages = repository?.languages?.nodes?.filter(
- (lang): lang is Language => lang != null,
- );
- const collaborators = repository?.collaborators?.nodes?.filter(
- (collaborator): collaborator is Collaborator => collaborator != null,
- );
return (
-
- {loading && (
-
- )}
- {repository?.description !== null ? (
- {repository?.description}
- ) : (
- No description, website, or topics provided.
- )}
-
- {repository?.homepageUrl && (
-
- {repository.homepageUrl}
-
- )}
- {languages?.map(({ id, name, color }) => (
-
- {name}
-
- ))}
-
- {collaborators && (
-
- {collaborators?.map(({ id, login, avatarUrl }) => (
-
-
-
- {login}
-
-
- ))}
-
- )}
-
+
+
+
+
+
);
-}
+};
export default RepoDetails;
diff --git a/src/features/repo-explorer/components/branches-menu/index.tsx b/src/features/repo-explorer/components/branches-menu/index.tsx
index 1016fe8..75bbaa9 100644
--- a/src/features/repo-explorer/components/branches-menu/index.tsx
+++ b/src/features/repo-explorer/components/branches-menu/index.tsx
@@ -1,19 +1,20 @@
import React from "react";
import { Menu } from "antd";
import { Link } from "react-router-dom";
-import { RepoIdentity } from "models";
+import { RepoIdentity, BranchIdentity } from "models";
import "./index.scss";
type Props = {
repo: RepoIdentity;
- branches?: Array<{ name: string; prefix: string }>;
+ branches?: Array;
+ onVisibleChange?: (flag: boolean) => void;
};
-function BranchesMenu({ repo, branches }: Props) {
+const BranchesMenu = ({ repo, branches, onVisibleChange }: Props) => {
return (
-