├── .github
└── ISSUE_TEMPLATE
│ ├── Bug_report.md
│ └── Enhancement.md
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .travis.yml
├── .vscode
└── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
├── octodirect_logo.png
├── octodirect_screenshot_1.png
├── octodirect_screenshot_2.png
└── octodirect_screenshot_3.png
├── config-overrides.js
├── package-lock.json
├── package.json
├── public
├── _locales
│ └── en
│ │ └── messages.json
├── index.html
├── manifest.json
├── octodirect_128.png
├── octodirect_16.png
└── octodirect_48.png
├── scripts
├── deploy.js
└── rollback.js
├── src
├── __tests__
│ └── unit
│ │ ├── actions
│ │ ├── actions.test.ts
│ │ └── createAction.test.ts
│ │ ├── components
│ │ └── input
│ │ │ └── getTargetUrl.test.ts
│ │ ├── reducers
│ │ ├── repos.reducers.test.ts
│ │ ├── setting-info.reducers.test.ts
│ │ └── view.reducers.test.ts
│ │ └── utils
│ │ ├── Array
│ │ ├── deleteItem.test.ts
│ │ └── filterByItem.test.ts
│ │ └── Key.test.ts
├── actions
│ ├── action-type-helper.ts
│ ├── actions.ts
│ └── createAction.ts
├── components
│ ├── main-view
│ │ ├── info
│ │ │ └── Info.tsx
│ │ ├── input
│ │ │ └── Input.tsx
│ │ ├── item
│ │ │ └── Item.tsx
│ │ └── itemlist
│ │ │ ├── ItemList.tsx
│ │ │ ├── ItemsLayout.tsx
│ │ │ ├── loading
│ │ │ └── Loading.tsx
│ │ │ └── not-found
│ │ │ └── NotFound.tsx
│ └── setting-view
│ │ ├── DomainSetting.tsx
│ │ ├── GitHubSetting.tsx
│ │ ├── Head.tsx
│ │ └── SettingView.tsx
├── container
│ ├── AppContainer.tsx
│ ├── MainContainer.tsx
│ └── SettingContainer.tsx
├── index.tsx
├── main
│ ├── App.tsx
│ └── appConfig.ts
├── model
│ └── item.model.ts
├── react-app-env.d.ts
├── reducers
│ ├── index.ts
│ ├── repos.reducers.ts
│ ├── setting-info.reducers.ts
│ └── view.reducers.ts
├── saga
│ ├── index.ts
│ └── repos.saga.ts
├── service
│ ├── browser-history.service.ts
│ ├── github-repository.service.ts
│ ├── setting.service.ts
│ └── user-info.service.ts
├── static
│ ├── images
│ │ └── octodirect_icon.png
│ └── styles
│ │ └── reset.css
├── storage
│ ├── ChromeStorage.ts
│ └── LocalStorage.ts
├── store
│ └── configureStore.ts
└── utils
│ ├── Array.ts
│ ├── DomUtils.ts
│ ├── Key.ts
│ ├── mock
│ └── mock.service.ts
│ └── throttle.ts
├── travis.yml
├── tsconfig.json
├── tsconfig.prod.json
├── tsconfig.test.json
└── tslint.json
/.github/ISSUE_TEMPLATE/Bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug report
3 | about: Report to bug or error!
4 | ---
5 |
6 | ## Description
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/Enhancement.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🌈 Enhancement
3 | about: Things you might want to try to improve or add to in your extension.
4 | ---
5 |
6 | ## Description
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # coveralls
24 | .coveralls.yml
25 |
26 | # jest
27 | /src/**/__snapshots__
28 |
29 | # production
30 | octodirect.zip
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tsconfig.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "useTabs": false,
4 | "singleQuote": true,
5 | "trailingComma": "all",
6 | "semi": false
7 | }
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | install:
5 | - npm install
6 | script:
7 | - npm run lint
8 | - npm run test
9 | after_success:
10 | - npm run coveralls
11 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": false,
3 | "eslint.alwaysShowStatus": true,
4 | "eslint.options": {
5 | "extensions": [
6 | ".html",
7 | ".ts",
8 | ".js",
9 | ".tsx"
10 | ]
11 | },
12 | "files.autoSave": "onFocusChange",
13 | "files.autoSaveDelay": 500,
14 | "typescript.tsdk": "node_modules/typescript/lib",
15 | "editor.codeActionsOnSave": {
16 | "source.fixAll.tslint": true
17 | }
18 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # HOW TO CONTRIBUTE
2 |
3 | # Issue
4 |
5 | Just follow `ISSUE_TEMPLATE`. done!
6 |
7 | # Pull Request
8 |
9 | ## Forked strategy
10 |
11 | This repository managed based on forked pull request strategy
12 |
13 | ```sh
14 | # Fort this repository to you
15 | $ git clone [YOUR_REPOSITORY_URL]
16 | $ cd octodirect
17 | $ npm install
18 | $ npm start
19 | # (Working...)
20 | $ git commit [...]
21 | $ git push origin [YOUR_REPOSITORY]
22 | # Enroll pull-request!
23 | ```
24 |
25 | ## Commit message rules
26 |
27 | Consider starting the commit message with an applicable emoji:
28 |
29 | - :recycle: `:recycle:` : with `env:` prefix.
30 | - when set new development environment.
31 | - :gift: `:gift:` : with `feat:` prefix.
32 | - when create new feature.
33 | - ✅ `:white_check_mark:` : with `test:` prefix.
34 | - when adding tests.
35 | - 🐛 `:bug:` : with `fix:` prefix.
36 | - when fixing a bug.
37 | - :memo: `:memo:` : with `docs:` prefix.
38 | - when add document.
39 |
40 | ### Thanks!
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jbee
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Octodirect [](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko) [](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko) [](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko)
6 |
7 | [](https://travis-ci.org/JaeYeopHan/octodirect)
8 | [](https://coveralls.io/github/JaeYeopHan/octodirect?branch=master)
9 | [](https://github.com/styled-components/styled-components)
10 | [](https://github.com/prettier/prettier)
11 | [](https://github.com/semantic-release/semantic-release)
12 | 
13 | [](https://opensource.org/licenses/MIT) [](https://greenkeeper.io/)
14 |
15 | This is Chrome extension.
16 |
17 | ### Directly, move your GitHub repository which...
18 |
19 | - ⚒️ you created. (max 100)
20 | - 🏠 you visited. (latest 100+)
21 | - ⭐ you starred. (latest 100)
22 |
23 | > If your 'input' does not exist in both lists, it will automatically search google with `github:` prefix, just type `enter`!.
24 |
25 |
26 |
27 | ## Usage
28 |
29 | ### [Chrome Web Store Download](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko)
30 |
31 | > `NOTE!` current version is beta.
32 |
33 | ### `cmd` + `shift` + `k`
34 |
35 |
36 |
37 | | Main | Domain Setting | GitHub Setting |
38 | | :------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------: | :------------------------------------------------------------------------: |
39 | |
|
|
|
40 | | You can use :arrow_up: :arrow_down: for search.
You can filter list by typing in `input`
You can move by :leftwards_arrow_with_hook: | Add domain prefix for curating.
like: `https://github.com`. | Add your account info to fetch
you created and starred repositories. |
41 |
42 |
43 |
44 |
45 | ##### `INFO: ` You can change hotkey in [chrome://extensions/shortcuts](chrome://extensions/shortcuts)
46 |
47 |
48 |
49 | ## Development Installation
50 |
51 | ```sh
52 | $ git clone https://github.com/JaeYeopHan/octodirect.git
53 | $ cd octodirect
54 | $ npm install
55 | $ npm start
56 | # npm test
57 | ```
58 |
59 |
60 |
61 | ## Release
62 | [release](https://github.com/JaeYeopHan/octodirect/releases)
63 |
64 |
65 |
66 | ## LICENSE
67 |
68 | [MIT](https://github.com/JaeYeopHan/octodirect/blob/master/LICENSE)
69 |
70 | ✌️
71 |
72 | A little project by @Jbee
73 |
74 |
--------------------------------------------------------------------------------
/assets/octodirect_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/assets/octodirect_logo.png
--------------------------------------------------------------------------------
/assets/octodirect_screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/assets/octodirect_screenshot_1.png
--------------------------------------------------------------------------------
/assets/octodirect_screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/assets/octodirect_screenshot_2.png
--------------------------------------------------------------------------------
/assets/octodirect_screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/assets/octodirect_screenshot_3.png
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const webpack = require('webpack')
4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
5 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
6 | .BundleAnalyzerPlugin
7 | const manifestJson = require('./public/manifest.json')
8 | const StringReplacePlugin = require('string-replace-webpack-plugin')
9 | const signale = require('signale')
10 |
11 | module.exports = (config, env) => {
12 | if (env === 'production') {
13 | config = rewireUglifyJS(config, env)
14 | } else {
15 | config = rewireBundleAnalyzer(config, env)
16 | }
17 | config = rewireStringReplace(config, env)
18 |
19 | return config
20 | }
21 |
22 | function rewireUglifyJS(config, env) {
23 | config.plugins.unshift(
24 | new UglifyJsPlugin({
25 | sourceMap: true,
26 | uglifyOptions: {
27 | compress: {
28 | drop_console: true,
29 | unused: true,
30 | dead_code: true,
31 | },
32 | },
33 | }),
34 | )
35 |
36 | return config
37 | }
38 |
39 | function rewireBundleAnalyzer(config, env) {
40 | config.plugins.push(new BundleAnalyzerPlugin())
41 | return config
42 | }
43 |
44 | function rewireStringReplace(config, env) {
45 | const version = env === 'production' ? manifestJson.version : '0.0.0'
46 |
47 | signale.info(`[INFO] octodirect version : ${version}`)
48 |
49 | config.module.rules.push({
50 | test: /(\.tsx)$/,
51 | loader: StringReplacePlugin.replace({
52 | replacements: [
53 | {
54 | pattern: /#__VERSION__#/gi,
55 | replacement: function(match, p1, offset, string) {
56 | return version
57 | },
58 | },
59 | ],
60 | }),
61 | })
62 | config.plugins.push(new StringReplacePlugin())
63 |
64 | return config
65 | }
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "octodirect",
3 | "author": "Jbee[ljyhanll@gmail.com]",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "autobind-decorator": "^2.1.0",
8 | "axios": "^0.19.0",
9 | "evergreen-ui": "^3.2.4",
10 | "lodash": "^4.17.19",
11 | "react": "^16.4.2",
12 | "react-dom": "^16.4.2",
13 | "react-redux": "^7.1.0",
14 | "react-scripts": "^2.1.1",
15 | "redux": "^4.0.0",
16 | "redux-logger": "^3.0.6",
17 | "redux-saga": "^1.0.2",
18 | "uglifyjs-webpack-plugin": "^2.1.1",
19 | "url-parse": "^1.4.4"
20 | },
21 | "scripts": {
22 | "deploy:real": "npm-run-all _versioning build _clean:zip _zipify dashboard",
23 | "rollback": "node ./scripts/rollback.js",
24 | "dashboard": "open https://chrome.google.com/webstore/devconsole",
25 | "typesync": "npx typesync && npm install",
26 | "coveralls": "react-scripts test --coverage && cat ./coverage/lcov.info | coveralls",
27 | "start": "react-app-rewired start",
28 | "build": "react-app-rewired build",
29 | "lint": "tslint --project ./tsconfig.json 'src/**/*.{ts,tsx}' --exclude \"**/__tests__/**\"",
30 | "test": "CI=true react-scripts test",
31 | "test:watch": "react-scripts test",
32 | "_clean:zip": "rm -rf octodirect.zip",
33 | "_zipify": "zip -r octodirect.zip build",
34 | "_versioning": "node ./scripts/deploy.js",
35 | "_eject": "react-scripts eject"
36 | },
37 | "devDependencies": {
38 | "@types/chrome": "0.0.89",
39 | "@types/enzyme": "^3.1.15",
40 | "@types/enzyme-adapter-react-16": "^1.0.3",
41 | "@types/jest": "^24.0.9",
42 | "@types/lodash-es": "^4.17.1",
43 | "@types/node": "^12.0.0",
44 | "@types/react": "^16.7.13",
45 | "@types/react-dom": "^16.0.11",
46 | "@types/react-redux": "^7.0.1",
47 | "@types/react-test-renderer": "^16.0.2",
48 | "@types/redux-logger": "^3.0.6",
49 | "@types/redux-saga": "^0.10.5",
50 | "@types/styled-components": "4.1.8",
51 | "@types/url-parse": "^1.4.2",
52 | "coveralls": "^3.0.2",
53 | "enzyme": "^3.8.0",
54 | "enzyme-adapter-react-16": "^1.2.0",
55 | "husky": "^3.0.0",
56 | "inquirer": "^6.1.0",
57 | "jest": "^23.6.0",
58 | "npm-run-all": "^4.1.3",
59 | "prettier": "^2.0.5",
60 | "react-app-rewired": "^2.0.2",
61 | "react-test-renderer": "^16.4.2",
62 | "signale": "^1.2.1",
63 | "string-replace-webpack-plugin": "^0.1.3",
64 | "styled-components": "^4.1.3",
65 | "ts-jest": "^24.0.0",
66 | "tslint": "^5.13.1",
67 | "tslint-clean-code": "^0.2.9",
68 | "tslint-config-prettier": "^1.17.0",
69 | "tslint-react": "^4.0.0",
70 | "tslint-sonarts": "^1.9.0",
71 | "typescript": "^3.2.2",
72 | "typescript-styled-plugin": "^0.14.0",
73 | "webpack-bundle-analyzer": "^3.8.0"
74 | },
75 | "husky": {
76 | "hooks": {
77 | "pre-commit": "npm run lint",
78 | "pre-push": "npm run test"
79 | }
80 | },
81 | "browserslist": [
82 | ">0.2%",
83 | "not dead",
84 | "not ie <= 11",
85 | "not op_mini all"
86 | ],
87 | "greenkeeper": {
88 | "ignore": [
89 | "evergreen-ui",
90 | "jest"
91 | ]
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/public/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "appDescription": {
3 | "description": "The description of the application",
4 | "message": "Enables CORS for websites that block CORS for debugging cross-origin request errors."
5 | },
6 | "appName": {
7 | "description": "The name of the application",
8 | "message": "Moesif CORS Utility"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
21 | React App
22 |
23 |
24 |
25 | You need to enable JavaScript to run this app.
26 |
27 |
28 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "octodirect",
4 | "version": "0.7.0",
5 | "description": "Move to GitHub repository directly",
6 | "browser_action": {
7 | "default_icon": "octodirect_48.png",
8 | "default_popup": "index.html"
9 | },
10 | "default_locale": "en",
11 | "icons": {
12 | "16": "octodirect_16.png",
13 | "48": "octodirect_48.png",
14 | "128": "octodirect_128.png"
15 | },
16 | "homepage_url": "https://github.com/JaeYeopHan/octodirect",
17 | "permissions": ["activeTab", "tabs", "history", "storage"],
18 | "commands": {
19 | "_execute_browser_action": {
20 | "suggested_key": {
21 | "windows": "Alt+Shift+K",
22 | "linux": "Alt+Shift+K",
23 | "chromeos": "Alt+Shift+K",
24 | "mac": "Command+Shift+K",
25 | "default": "Ctrl+Shift+K"
26 | },
27 | "description": "open_popup"
28 | }
29 | },
30 | "content_security_policy":
31 | "script-src 'self' 'sha256-5As4+3YpY62+l38PsxCEkjB1R4YtyktBtRScTJ3fyLU='; object-src 'self'"
32 | }
33 |
--------------------------------------------------------------------------------
/public/octodirect_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/public/octodirect_128.png
--------------------------------------------------------------------------------
/public/octodirect_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/public/octodirect_16.png
--------------------------------------------------------------------------------
/public/octodirect_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/public/octodirect_48.png
--------------------------------------------------------------------------------
/scripts/deploy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const inquirer = require('inquirer')
5 | const signale = require('signale')
6 | const { promisify } = require('util')
7 | const exec = promisify(require('child_process').exec)
8 |
9 | const manifestPath = './public/manifest.json'
10 | const manifestJsonFile = fs.readFileSync(manifestPath, {
11 | encoding: 'utf-8',
12 | })
13 | const manifestJson = JSON.parse(manifestJsonFile)
14 | const version = manifestJson.version
15 |
16 | signale.note(`Current version: v${version}`)
17 |
18 | inquirer
19 | .prompt([
20 | {
21 | type: 'list',
22 | name: 'type',
23 | message: 'Select deploy type: ',
24 | choices: ['patch', 'minor', 'major'],
25 | },
26 | ])
27 | .then(async ({ type }) => {
28 | const updatedVersion = updateVersion(type, version)
29 |
30 | manifestJson.version = updatedVersion
31 |
32 | fs.writeFileSync(manifestPath, JSON.stringify(manifestJson))
33 |
34 | signale.note(`=> New version: v${updatedVersion}\n`)
35 |
36 | await applyPrettier()
37 | signale.note(`Complete to update version. start to release!\n`)
38 | await release(updatedVersion)
39 | })
40 |
41 | function updateVersion(type, version) {
42 | const splittedVersion = version.split('.')
43 | const patchTarget = splittedVersion[2]
44 | const minorTarget = splittedVersion[1]
45 | const majorTarget = splittedVersion[0]
46 |
47 | switch (type) {
48 | case 'patch':
49 | return [majorTarget, minorTarget, Number(patchTarget) + 1].join('.')
50 | case 'minor':
51 | return [majorTarget, Number(minorTarget) + 1, 0].join('.')
52 | case 'major':
53 | return [Number(majorTarget) + 1, 0, 0].join('.')
54 | }
55 | }
56 |
57 | async function release(version) {
58 | try {
59 | await commit(version)
60 | await tag(version)
61 | await push()
62 | await pushTag()
63 | signale.success('release completed!')
64 | } catch (e) {
65 | signale.warn('Fail to commit or push!', e)
66 | return false
67 | }
68 | }
69 |
70 | async function commit(version) {
71 | return exec(
72 | [
73 | `git commit`,
74 | `--allow-empty`,
75 | `-m ':tada: v${version}'`,
76 | `${manifestPath}`,
77 | ].join(' '),
78 | )
79 | }
80 |
81 | async function push() {
82 | return exec('git push')
83 | }
84 |
85 | async function tag(version) {
86 | return exec(`git tag ${version}`)
87 | }
88 |
89 | async function pushTag() {
90 | return exec(`git push --tags`)
91 | }
92 |
93 | async function applyPrettier() {
94 | return exec('prettier --write ./public/manifest.json')
95 | }
96 |
--------------------------------------------------------------------------------
/scripts/rollback.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const signale = require('signale')
5 | const { promisify } = require('util')
6 | const exec = promisify(require('child_process').exec)
7 | const manifestPath = './public/manifest.json'
8 | const manifestJsonFile = fs.readFileSync(manifestPath, {
9 | encoding: 'utf-8',
10 | })
11 | const manifestJson = JSON.parse(manifestJsonFile)
12 | const currentVersion = manifestJson.version
13 |
14 | async function removeTag(version) {
15 | return exec(`git tag -d ${version}`)
16 | }
17 |
18 | async function removeRemoteTag(version) {
19 | return exec(`git push -d origin ${version}`)
20 | }
21 |
22 | async function revert() {
23 | return exec(`git reset HEAD^`)
24 | }
25 |
26 | async function reset() {
27 | return exec(`git checkout -- .`)
28 | }
29 |
30 | async function forcedPush() {
31 | return exec('git push -f')
32 | }
33 |
34 | ;(async function rollback() {
35 | signale.info(`Rollback!`)
36 |
37 | await removeTag(currentVersion)
38 | await removeRemoteTag(currentVersion)
39 | await revert()
40 | await reset()
41 | await forcedPush()
42 |
43 | const manifestJsonFile = fs.readFileSync(manifestPath, {
44 | encoding: 'utf-8',
45 | })
46 | const manifestJson = JSON.parse(manifestJsonFile)
47 | const version = manifestJson.version
48 |
49 | signale.note(`Current version: v${version}`)
50 | })()
51 |
--------------------------------------------------------------------------------
/src/__tests__/unit/actions/actions.test.ts:
--------------------------------------------------------------------------------
1 | import { ActionTypes, actions } from '../../../actions/actions';
2 | import { FetchResponseType, FetchDataResponse } from '../../../saga/repos.saga';
3 | import { UserInfoInterface } from '../../../service/user-info.service';
4 |
5 | describe('/actions/actions', () => {
6 | it('INCREMENT_INDEX, DECREMENT_INDEX', () => {
7 | // Given
8 | const incrementIndex = {
9 | type: ActionTypes.INCREMENT_INDEX,
10 | };
11 | const decrementIndex = {
12 | type: ActionTypes.DECREMENT_INDEX,
13 | };
14 |
15 | // When
16 |
17 | // Then
18 | expect(actions.incrementIndex()).toEqual(incrementIndex);
19 | expect(actions.decrementIndex()).toEqual(decrementIndex);
20 | });
21 |
22 | it('UPDATE_VALUE', () => {
23 | // Given
24 | const mockPayload = 'mock';
25 | const updateValue = {
26 | type: ActionTypes.UPDATE_VALUE,
27 | payload: mockPayload,
28 | };
29 |
30 | // When
31 |
32 | // Then
33 | expect(actions.updateValue(mockPayload)).toEqual(updateValue);
34 | });
35 |
36 | it('FETCH_REQUEST, FETCH_SUCCESS, FETCH_FAIL', () => {
37 | // Given
38 | const mockPayload: FetchDataResponse = {
39 | response: FetchResponseType.FETCH_READY,
40 | data: [],
41 | };
42 | const fetchRequest = {
43 | type: ActionTypes.FETCH_REQUEST,
44 | };
45 | const fetchSuccess = {
46 | type: ActionTypes.FETCH_SUCCESS,
47 | payload: mockPayload,
48 | };
49 | const fetchFail = {
50 | type: ActionTypes.FETCH_FAIL,
51 | payload: mockPayload,
52 | };
53 |
54 | // When
55 |
56 | // Then
57 | expect(actions.fetchRequest()).toEqual(fetchRequest);
58 | expect(actions.fetchSuccess(mockPayload)).toEqual(fetchSuccess);
59 | expect(actions.fetchFail(mockPayload)).toEqual(fetchFail);
60 | });
61 |
62 | it('TOGGLE_VIEW', () => {
63 | // Given
64 | const toggleView = {
65 | type: ActionTypes.TOGGLE_VIEW,
66 | };
67 |
68 | // When
69 |
70 | // Then
71 | expect(toggleView).toEqual(actions.toggleView());
72 | });
73 |
74 | it('INSERT_USERINFO', () => {
75 | // Given
76 | const mockPayload: UserInfoInterface = {};
77 | const insertUserInfo = {
78 | type: ActionTypes.INSERT_USERINFO,
79 | payload: mockPayload,
80 | };
81 |
82 | // When
83 |
84 | // Then
85 | expect(actions.insertUserInfo(mockPayload)).toEqual(insertUserInfo);
86 | });
87 |
88 | it('INSERT_DOMAININFO, DELETE_DOMAININFO', () => {
89 | // Given
90 | const mockPayload = '';
91 | const insertDomainInfo = {
92 | type: ActionTypes.INSERT_DOMAININFO,
93 | payload: mockPayload,
94 | };
95 | const deleteDomainInfo = {
96 | type: ActionTypes.DELETE_DOMAININFO,
97 | payload: mockPayload,
98 | };
99 |
100 | // When
101 |
102 | // Then
103 | expect(actions.insertDomainInfo(mockPayload)).toEqual(insertDomainInfo);
104 | expect(actions.deleteDomainInfo(mockPayload)).toEqual(deleteDomainInfo);
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/src/__tests__/unit/actions/createAction.test.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '../../../actions/createAction';
2 |
3 | describe('/actions', () => {
4 | it('createAction() with only type', () => {
5 | // Given
6 | const actionType = 'testAction';
7 |
8 | // When
9 | const result = createAction(actionType);
10 |
11 | // Then
12 | expect(result).toEqual({ type: actionType });
13 | });
14 |
15 | it('createAction() with payload', () => {
16 | // Given
17 | const actionType = 'testAction';
18 | const payload = { testData: 'testData' };
19 |
20 | // When
21 | const result = createAction(actionType, payload);
22 |
23 | // Then
24 | expect(result).toEqual({
25 | type: actionType,
26 | payload,
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/__tests__/unit/components/input/getTargetUrl.test.ts:
--------------------------------------------------------------------------------
1 | import { getTargetUrl } from '../../../../components/main-view/input/Input'
2 | import { ItemType } from '../../../../model/item.model'
3 | import { GOOGLE_SEARCH_URL } from '../../../../main/appConfig'
4 |
5 | test('return google search when same index with filtered length', () => {
6 | // Given
7 | const filtered = [
8 | { id: '1', name: 'name1', htmlUrl: 'test-url-1' },
9 | { id: '2', name: 'name2', htmlUrl: 'test-url-2' },
10 | { id: '3', name: 'name3', htmlUrl: 'test-url-3' },
11 | ]
12 | const value = ''
13 | const index = 3
14 | const inputValue = 'test-input-value'
15 |
16 | // When
17 | const result = getTargetUrl(filtered, value, index, inputValue)
18 |
19 | // Then
20 | expect(result).toBe(`${GOOGLE_SEARCH_URL}${inputValue}`)
21 | })
22 |
23 | test('return google search when empty filtered', () => {
24 | // Given
25 | const filtered = [] as ItemType[]
26 | const value = 'test-value'
27 | const index = 3
28 | const inputValue = 'test'
29 |
30 | // When
31 | const result = getTargetUrl(filtered, value, index, inputValue)
32 |
33 | // Then
34 | expect(result).toBe(`${GOOGLE_SEARCH_URL}${value}`)
35 | })
36 |
37 | test('return target url', () => {
38 | // Given
39 | const filtered = [
40 | { id: '1', name: 'name1', htmlUrl: 'test-url-1' },
41 | { id: '2', name: 'name2', htmlUrl: 'test-url-2' },
42 | { id: '3', name: 'name3', htmlUrl: 'test-url-3' },
43 | ]
44 | const value = 'test'
45 | const index = 0
46 | const inputValue = ''
47 |
48 | // When
49 | const result = getTargetUrl(filtered, value, index, inputValue)
50 |
51 | // Then
52 | expect(result).toBe(filtered[index].htmlUrl)
53 | })
54 |
--------------------------------------------------------------------------------
/src/__tests__/unit/reducers/repos.reducers.test.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-duplicate-string
2 | import {
3 | RepoState,
4 | reposReducers,
5 | refineData,
6 | getRepoId,
7 | } from '../../../reducers/repos.reducers'
8 | import { FetchResponseType, FetchDataResponse } from '../../../saga/repos.saga'
9 | import { actions } from '../../../actions/actions'
10 | import { RepositoryInfo } from '../../../service/github-repository.service'
11 | import { ItemType } from '../../../model/item.model'
12 |
13 | function getInitialState(): RepoState {
14 | return {
15 | list: [],
16 | filtered: [],
17 | index: 0,
18 | value: '',
19 | maxIndex: 0,
20 | fetchResponseType: FetchResponseType.FETCH_READY,
21 | }
22 | }
23 |
24 | test('fetchSuccess() when empty array', () => {
25 | // Given
26 | const state = getInitialState()
27 | const payload: FetchDataResponse = {
28 | response: FetchResponseType.SUCCESS,
29 | data: [],
30 | }
31 | const fetchSuccess = actions.fetchSuccess(payload)
32 |
33 | // When
34 | const result = reposReducers(state, fetchSuccess)
35 |
36 | // Then
37 | const expected = {
38 | ...state,
39 | ...{
40 | fetchResponseType: payload.response,
41 | },
42 | }
43 | expect(result).toEqual(expected)
44 | })
45 |
46 | test('fetchSuccess() when refined array', () => {
47 | // Given
48 | const state = getInitialState()
49 | const payload: FetchDataResponse = {
50 | response: FetchResponseType.SUCCESS,
51 | data: [
52 | { id: '1', name: 'name1', url: 'https://github.com/JaeYeopHan/test1' },
53 | { id: '2', name: 'name2', url: 'https://github.com/JaeYeopHan/test2' },
54 | {
55 | id: '33',
56 | name: 'name33',
57 | url: 'https://github.com/JaeYeopHan/test3/test33',
58 | },
59 | { id: '3', name: 'name3', url: 'https://github.com/JaeYeopHan/test3' },
60 | ],
61 | }
62 | const fetchSuccess = actions.fetchSuccess(payload)
63 |
64 | // When
65 | const result = reposReducers(state, fetchSuccess)
66 |
67 | // Then
68 | const expected = {
69 | ...state,
70 | ...{
71 | fetchResponseType: FetchResponseType.SUCCESS,
72 | filtered: [
73 | {
74 | id: '1',
75 | name: 'name1',
76 | htmlUrl: 'https://github.com/JaeYeopHan/test1',
77 | },
78 | {
79 | id: '2',
80 | name: 'name2',
81 | htmlUrl: 'https://github.com/JaeYeopHan/test2',
82 | },
83 | {
84 | id: '3',
85 | name: 'name3',
86 | htmlUrl: 'https://github.com/JaeYeopHan/test3',
87 | },
88 | ],
89 | index: 0,
90 | list: [
91 | {
92 | id: '1',
93 | name: 'name1',
94 | htmlUrl: 'https://github.com/JaeYeopHan/test1',
95 | },
96 | {
97 | id: '2',
98 | name: 'name2',
99 | htmlUrl: 'https://github.com/JaeYeopHan/test2',
100 | },
101 | {
102 | id: '3',
103 | name: 'name3',
104 | htmlUrl: 'https://github.com/JaeYeopHan/test3',
105 | },
106 | ],
107 | maxIndex: 3,
108 | },
109 | }
110 | expect(result).toEqual(expected)
111 | })
112 |
113 | test('incrementIndex() when empty array', () => {
114 | // Given
115 | const state = getInitialState()
116 |
117 | // When
118 | const INCREMENT_INDEX = actions.incrementIndex()
119 | const result = reposReducers(state, INCREMENT_INDEX)
120 |
121 | // Then
122 | expect(result).toEqual({
123 | ...state,
124 | fetchResponseType: 'FETCH_READY',
125 | filtered: [],
126 | index: 1,
127 | list: [],
128 | maxIndex: 0,
129 | value: '',
130 | })
131 | })
132 |
133 | test('incrementIndex() when normal status', () => {
134 | // Given
135 | const state = {
136 | ...getInitialState(),
137 | list: [
138 | {
139 | id: '1',
140 | name: 'name1',
141 | htmlUrl: 'https://github.com/JaeYeopHan/test1',
142 | },
143 | {
144 | id: '2',
145 | name: 'name2',
146 | htmlUrl: 'https://github.com/JaeYeopHan/test2',
147 | },
148 | ],
149 | }
150 |
151 | // When
152 | const INCREMENT_INDEX = actions.incrementIndex()
153 | const result = reposReducers(state, INCREMENT_INDEX)
154 |
155 | // Then
156 | expect(result).toEqual({
157 | ...state,
158 | ...{
159 | fetchResponseType: 'FETCH_READY',
160 | filtered: [],
161 | index: 1,
162 | list: [
163 | {
164 | id: '1',
165 | name: 'name1',
166 | htmlUrl: 'https://github.com/JaeYeopHan/test1',
167 | },
168 | {
169 | id: '2',
170 | name: 'name2',
171 | htmlUrl: 'https://github.com/JaeYeopHan/test2',
172 | },
173 | ],
174 | maxIndex: 0,
175 | value: 'name2',
176 | },
177 | })
178 | })
179 |
180 | test('refinedData return empty array', () => {
181 | // Given
182 |
183 | // When
184 | const result = refineData((null as unknown) as RepositoryInfo[])
185 |
186 | // Then
187 | expect(result).toEqual([])
188 | })
189 |
190 | test('refinedData return refined ItemType array', () => {
191 | // Given
192 | const data: RepositoryInfo[] = [{ id: '1', name: 'name', url: 'htmlUrl' }]
193 |
194 | // When
195 | const result = refineData(data)
196 |
197 | // Then
198 | expect(result).toEqual([{ id: '1', name: 'name', htmlUrl: 'htmlUrl' }])
199 | })
200 |
201 | test('getRepoId return id', () => {
202 | // Given
203 | const item: ItemType = {
204 | id: '1',
205 | name: 'name',
206 | htmlUrl: 'https://www.naver.com/page?edit=true',
207 | }
208 |
209 | // When
210 | const result = getRepoId(item)
211 |
212 | // Then
213 | expect(result).toEqual('/page')
214 | })
215 |
--------------------------------------------------------------------------------
/src/__tests__/unit/reducers/setting-info.reducers.test.ts:
--------------------------------------------------------------------------------
1 | import { UserInfoInterface } from '../../../service/user-info.service';
2 | import { DomainInfoInterface } from '../../../service/setting.service';
3 | import {
4 | SettingInfoState,
5 | settingInfoReducers,
6 | } from '../../../reducers/setting-info.reducers';
7 | import { actions } from '../../../actions/actions';
8 |
9 | describe('/reducers/setting-info.reducers', () => {
10 | beforeEach(() => {
11 | localStorage.clear();
12 | });
13 |
14 | function getInitialState(
15 | userInfo: UserInfoInterface = { name: 'test', token: '' },
16 | domainInfo: DomainInfoInterface = ['testurl'],
17 | ): SettingInfoState {
18 | return {
19 | userInfo,
20 | domainInfo,
21 | };
22 | }
23 |
24 | it('insertUserInfo()', () => {
25 | // Given
26 | const state = getInitialState();
27 | const userInfo = {
28 | name: 'Jbee',
29 | token: 'jbee-token',
30 | };
31 | const insertUserInfo = actions.insertUserInfo(userInfo);
32 |
33 | // When
34 | const result = settingInfoReducers(state, insertUserInfo);
35 |
36 | // Then
37 | const expected = {
38 | ...state,
39 | ...{
40 | userInfo,
41 | },
42 | };
43 | expect(result).toEqual(expected);
44 | });
45 |
46 | it('insertDomainInfo()', () => {
47 | // Given
48 | const state = getInitialState();
49 | const newDomainInfo = 'testUrl';
50 | const insertDomainInfo = actions.insertDomainInfo(newDomainInfo);
51 | const domainInfo = state.domainInfo.concat(newDomainInfo);
52 |
53 | // When
54 | const result = settingInfoReducers(state, insertDomainInfo);
55 |
56 | // Then
57 | const expected = {
58 | ...state,
59 | domainInfo,
60 | };
61 | expect(result).toEqual(expected);
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/src/__tests__/unit/reducers/view.reducers.test.ts:
--------------------------------------------------------------------------------
1 | import { actions } from '../../../actions/actions'
2 | import { viewReducers, ViewState } from '../../../reducers/view.reducers'
3 |
4 | describe('/reducers/view.reducers', () => {
5 | function getInitialState(viewType: any): ViewState {
6 | return {
7 | type: viewType,
8 | }
9 | }
10 |
11 | it('toggleView(), main to setting', () => {
12 | // Given
13 | const state = getInitialState('main')
14 | const toggleView = actions.toggleView()
15 |
16 | // When
17 | const result = viewReducers(state, toggleView)
18 |
19 | // Then
20 | const expected = {
21 | type: 'setting',
22 | }
23 | expect(result).toEqual(expected)
24 | })
25 |
26 | // tslint:disable-next-line:no-identical-functions
27 | it('toggleView(), setting to main', () => {
28 | // Given
29 | const state = getInitialState('setting')
30 | const toggleView = actions.toggleView()
31 |
32 | // When
33 | const result = viewReducers(state, toggleView)
34 |
35 | // Then
36 | const expected = {
37 | type: 'main',
38 | }
39 | expect(result).toEqual(expected)
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/src/__tests__/unit/utils/Array/deleteItem.test.ts:
--------------------------------------------------------------------------------
1 | import { deleteItem } from '../../../../utils/Array';
2 |
3 | describe('utils/Array.ts', () => {
4 | it('deleteItem() when empty array', () => {
5 | // Given
6 | const testArray: any = [];
7 | const target = 'dummy';
8 |
9 | // When
10 | const results = deleteItem(testArray, target);
11 | const expected: any = [];
12 |
13 | // Then
14 | expect(results).toEqual(expected);
15 | });
16 |
17 | it('deleteItem() when length 1 array, do delete', () => {
18 | // Given
19 | const testArray = ['dummy'];
20 | const target = 'dummy';
21 |
22 | // When
23 | const results = deleteItem(testArray, target);
24 | const expected: any = [];
25 |
26 | // Then
27 | expect(results).toEqual(expected);
28 | });
29 |
30 | it('deleteItem() when length 1 array do not delete', () => {
31 | // Given
32 | const testArray = ['dummy'];
33 | const target = 'd';
34 |
35 | // When
36 | const results = deleteItem(testArray, target);
37 | const expected: any = testArray;
38 |
39 | // Then
40 | expect(results).toEqual(expected);
41 | });
42 |
43 | it('deleteItem() when length 2 array do delete', () => {
44 | // Given
45 | const testArray = ['dummy', 'mock'];
46 | const target = 'dummy';
47 |
48 | // When
49 | const results = deleteItem(testArray, target);
50 | const expected: any = ['mock'];
51 |
52 | // Then
53 | expect(results).toEqual(expected);
54 | });
55 |
56 | it('deleteItem() when length 2 array do not delete', () => {
57 | // Given
58 | const testArray = ['dummy', 'mock'];
59 | const target = 'd';
60 |
61 | // When
62 | const results = deleteItem(testArray, target);
63 | const expected: any = ['dummy', 'mock'];
64 |
65 | // Then
66 | expect(results).toEqual(expected);
67 | });
68 |
69 | it('deleteItem() when length 3 array do delete first element', () => {
70 | // Given
71 | const testArray = ['a', 'b', 'c'];
72 | const target = 'a';
73 |
74 | // When
75 | const results = deleteItem(testArray, target);
76 | const expected = ['b', 'c'];
77 |
78 | // Then
79 | expect(results).toEqual(expected);
80 | });
81 |
82 | it('deleteItem() when length 3 array do delete middle element', () => {
83 | // Given
84 | const testArray = ['a', 'b', 'c'];
85 | const target = 'b';
86 |
87 | // When
88 | const results = deleteItem(testArray, target);
89 | const expected = ['a', 'c'];
90 |
91 | // Then
92 | expect(results).toEqual(expected);
93 | });
94 |
95 | it('deleteItem() when length 3 array do delete last element', () => {
96 | // Given
97 | const testArray = ['a', 'b', 'c'];
98 | const target = 'c';
99 |
100 | // When
101 | const results = deleteItem(testArray, target);
102 | const expected = ['a', 'b'];
103 |
104 | // Then
105 | expect(results).toEqual(expected);
106 | });
107 |
108 | it('deleteItem() when length 4 array do delete first element', () => {
109 | // Given
110 | const testArray = ['a', 'b', 'c', 'd'];
111 | const target = 'a';
112 |
113 | // When
114 | const results = deleteItem(testArray, target);
115 | const expected = ['b', 'c', 'd'];
116 |
117 | // Then
118 | expect(results).toEqual(expected);
119 | });
120 |
121 | it('deleteItem() when length 4 array do delete middle element', () => {
122 | // Given
123 | const testArray = ['a', 'b', 'c', 'd'];
124 | const target = 'b';
125 |
126 | // When
127 | const results = deleteItem(testArray, target);
128 | const expected = ['a', 'c', 'd'];
129 |
130 | // Then
131 | expect(results).toEqual(expected);
132 | });
133 |
134 | it('deleteItem() when length 4 array do delete last element', () => {
135 | // Given
136 | const testArray = ['a', 'b', 'c', 'd'];
137 | const target = 'd';
138 |
139 | // When
140 | const results = deleteItem(testArray, target);
141 | const expected = ['a', 'b', 'c'];
142 |
143 | // Then
144 | expect(results).toEqual(expected);
145 | });
146 |
147 | it('deleteItem() when length 4 array do not delete element', () => {
148 | // Given
149 | const testArray = ['a', 'b', 'c', 'd'];
150 | const target = 'e';
151 |
152 | // When
153 | const results = deleteItem(testArray, target);
154 | const expected = ['a', 'b', 'c', 'd'];
155 |
156 | // Then
157 | expect(results).toEqual(expected);
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/src/__tests__/unit/utils/Array/filterByItem.test.ts:
--------------------------------------------------------------------------------
1 | import { filterByItem } from '../../../../utils/Array';
2 |
3 | describe('utils/Array.ts', () => {
4 | type Item = {
5 | id: number;
6 | name: string;
7 | };
8 |
9 | const mockData: Item[] = [
10 | { id: 1, name: 'one' },
11 | { id: 2, name: 'two' },
12 | { id: 3, name: 'three' },
13 | ];
14 |
15 | it('filterByItem() when empty string value', () => {
16 | // Given
17 | const value = '';
18 |
19 | // When
20 | const result = filterByItem(mockData, value);
21 |
22 | // Then
23 | const expected = mockData;
24 | expect(result).toEqual(expected);
25 | });
26 |
27 | it('filterByItem() when uppercase value', () => {
28 | // Given
29 | const value = 'ONE';
30 |
31 | // When
32 | const result = filterByItem(mockData, value);
33 |
34 | // Then
35 | const expected = [{ id: 1, name: 'one' }];
36 | expect(result).toEqual(expected);
37 | });
38 |
39 | it('filterByItem() when single value', () => {
40 | // Given
41 | const value = 'e';
42 |
43 | // When
44 | const result = filterByItem(mockData, value);
45 |
46 | // Then
47 | const expected = [{ id: 1, name: 'one' }, { id: 3, name: 'three' }];
48 | expect(result).toEqual(expected);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/__tests__/unit/utils/Key.test.ts:
--------------------------------------------------------------------------------
1 | import { KeyUtils } from '../../../utils/Key'
2 |
3 | describe('utils/Key.ts', () => {
4 | it('KeyUtils.isCorrectUpKey to false because of index', () => {
5 | // Given
6 | const keyCode = 38
7 | const index = 0
8 |
9 | // When
10 | const result = KeyUtils.isCorrectUpKey(keyCode, index)
11 |
12 | // Then
13 | expect(result).toBe(false)
14 | })
15 |
16 | it('KeyUtils.isCorrectUpKey to true', () => {
17 | // Given
18 | const keyCode = 38
19 | const index = 1
20 |
21 | // When
22 | const result = KeyUtils.isCorrectUpKey(keyCode, index)
23 |
24 | // Then
25 | expect(result).toBe(true)
26 | })
27 |
28 | it('KeyUtils.isCorrectDownKey to true', () => {
29 | // Given
30 | const keyCode = 40
31 | const index = 1
32 | const maxIndex = 30
33 |
34 | // When
35 | const result = KeyUtils.isCorrectDownKey(keyCode, index, maxIndex)
36 |
37 | // Then
38 | expect(result).toBe(true)
39 | })
40 |
41 | it('KeyUtils.isCorrectEnterKey to true', () => {
42 | // Given
43 | const keyCode = 13
44 |
45 | // When
46 | const result = KeyUtils.isEnterKey(keyCode)
47 |
48 | // Then
49 | expect(result).toBe(true)
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/src/actions/action-type-helper.ts:
--------------------------------------------------------------------------------
1 | export type FunctionType = (...args: any[]) => any
2 |
3 | export type ActionCreatorsMapObject = { [actionCreator: string]: FunctionType }
4 |
5 | export type ActionsUnion = ReturnType<
6 | A[keyof A]
7 | >
8 |
--------------------------------------------------------------------------------
/src/actions/actions.ts:
--------------------------------------------------------------------------------
1 | import { UserInfoInterface } from '../service/user-info.service'
2 | import { createAction } from './createAction'
3 | import { FetchDataResponse } from '../saga/repos.saga'
4 | import { ActionsUnion } from './action-type-helper'
5 |
6 | export enum ActionTypes {
7 | FETCH_REQUEST = 'FETCH_REQUEST',
8 | FETCH_SUCCESS = 'FETCH_SUCCESS',
9 | FETCH_FAIL = 'FETCH_FAIL',
10 |
11 | INCREMENT_INDEX = 'INCREMENT_INDEX',
12 | DECREMENT_INDEX = 'DECREMENT_INDEX',
13 |
14 | UPDATE_VALUE = 'UPDATE_VALUE',
15 | TOGGLE_VIEW = 'TOGGLE_VIEW',
16 |
17 | INSERT_USERINFO = 'INSERT_USERINFO',
18 |
19 | INSERT_DOMAININFO = 'INSERT_DOMAININFO',
20 | DELETE_DOMAININFO = 'DELETE_DOMAININFO',
21 | }
22 |
23 | export const actions = {
24 | incrementIndex: () => createAction(ActionTypes.INCREMENT_INDEX),
25 | decrementIndex: () => createAction(ActionTypes.DECREMENT_INDEX),
26 |
27 | updateValue: (value: string) => createAction(ActionTypes.UPDATE_VALUE, value),
28 |
29 | fetchRequest: () => createAction(ActionTypes.FETCH_REQUEST),
30 | fetchSuccess: (response: FetchDataResponse) =>
31 | createAction(ActionTypes.FETCH_SUCCESS, response),
32 | fetchFail: (response: FetchDataResponse) =>
33 | createAction(ActionTypes.FETCH_FAIL, response),
34 |
35 | toggleView: () => createAction(ActionTypes.TOGGLE_VIEW),
36 |
37 | insertUserInfo: (info: UserInfoInterface) =>
38 | createAction(ActionTypes.INSERT_USERINFO, info),
39 |
40 | insertDomainInfo: (info: string) =>
41 | createAction(ActionTypes.INSERT_DOMAININFO, info),
42 | deleteDomainInfo: (info: string) =>
43 | createAction(ActionTypes.DELETE_DOMAININFO, info),
44 | }
45 |
46 | export type Actions = ActionsUnion
47 |
--------------------------------------------------------------------------------
/src/actions/createAction.ts:
--------------------------------------------------------------------------------
1 | export interface Action {
2 | type: T
3 | }
4 |
5 | export interface ActionWithPayload extends Action {
6 | payload: P
7 | }
8 |
9 | export function createAction(type: T): Action
10 | export function createAction(
11 | type: T,
12 | payload: P,
13 | ): ActionWithPayload
14 | export function createAction(type: T, payload?: P) {
15 | return payload === undefined ? { type } : { type, payload }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/main-view/info/Info.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { FetchResponseType } from '../../../saga/repos.saga'
4 |
5 | const Container = styled.div`
6 | position: relative;
7 | width: calc(100% - 20px);
8 | margin: auto;
9 | margin-top: 52px;
10 | margin-bottom: 8px;
11 | `
12 |
13 | const Logo = styled.span`
14 | margin-left: 4px;
15 | font-size: 12px;
16 | color: #666;
17 | :hover {
18 | color: #0366d6;
19 | cursor: pointer;
20 | }
21 | `
22 |
23 | const Button = styled.a`
24 | display: inline-block;
25 | position: absolute;
26 | right: 0px;
27 | color: #0366d6;
28 | font-size: 14px;
29 | :hover {
30 | color: #0366d6;
31 | text-decoration: none;
32 | cursor: pointer;
33 | }
34 | @keyframes flutter {
35 | 0% {
36 | transform: rotate(0deg);
37 | }
38 | 35% {
39 | transform: rotate(0deg);
40 | }
41 | 40% {
42 | transform: rotate(-5deg);
43 | }
44 | 60% {
45 | transform: rotate(5deg);
46 | }
47 | 65% {
48 | transform: rotate(0deg);
49 | }
50 | 100% {
51 | transform: rotate(0deg);
52 | }
53 | }
54 | &.ani {
55 | transform-origin: center;
56 | animation: flutter 2s infinite linear;
57 | }
58 | `
59 |
60 | interface InfoProps {
61 | onToggleView: () => void
62 | authStatus: FetchResponseType
63 | }
64 |
65 | const targetUrl = 'http://github.com/JaeYeopHan/octodirect'
66 |
67 | export const Info: React.SFC = ({ onToggleView, authStatus }) => {
68 | const handleClick = () => {
69 | if (process.env.NODE_ENV === 'development') {
70 | console.log(`redirect to ${targetUrl}`)
71 | } else {
72 | chrome.tabs.create({ url: targetUrl })
73 | setTimeout(() => window.close, 300)
74 | }
75 | }
76 |
77 | return (
78 |
79 | handleClick()}>octodirect@#__VERSION__#
80 |
84 | Setting
85 |
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/main-view/input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { KeyUtils } from '../../../utils/Key'
4 | import { RepoState } from '../../../reducers/repos.reducers'
5 | import { ItemType } from '../../../model/item.model'
6 | import { GOOGLE_SEARCH_URL } from '../../../main/appConfig'
7 |
8 | const StyledInput = styled.input`
9 | position: fixed;
10 | top: 8px;
11 | padding: 4px 12px;
12 | width: 240px;
13 | color: #cad5df;
14 | font-size: 16px;
15 | border: none;
16 | ::placeholder {
17 | color: grey;
18 | }
19 | `
20 |
21 | interface InputProps {
22 | repos: RepoState
23 | currentTargetValue: string
24 | onPressUpKey: () => void
25 | onPressDownKey: () => void
26 | onChange: (value: string) => void
27 | openTarget: (url: string) => void
28 | }
29 |
30 | export class Input extends React.Component {
31 | public render(): JSX.Element {
32 | const { value } = this.props.repos
33 |
34 | return (
35 | ) =>
39 | this.handleKeyDown(event)
40 | }
41 | onChange={(event: React.ChangeEvent) =>
42 | this.handleChange(event)
43 | }
44 | />
45 | )
46 | }
47 |
48 | private handleKeyDown({
49 | keyCode,
50 | currentTarget,
51 | }: React.KeyboardEvent): void {
52 | const { openTarget, repos, currentTargetValue } = this.props
53 | const { index, maxIndex, filtered } = repos
54 | const lastIndex = currentTargetValue ? maxIndex : maxIndex - 1
55 |
56 | if (KeyUtils.isCorrectUpKey(keyCode, index)) {
57 | return this.props.onPressUpKey()
58 | }
59 |
60 | if (KeyUtils.isCorrectDownKey(keyCode, index, lastIndex)) {
61 | this.props.onPressDownKey()
62 | return
63 | }
64 |
65 | if (KeyUtils.isEnterKey(keyCode)) {
66 | const { placeholder: value } = currentTarget
67 | const targetUrl = getTargetUrl(
68 | filtered,
69 | value,
70 | index,
71 | this.props.currentTargetValue,
72 | )
73 |
74 | if (process.env.NODE_ENV === 'development') {
75 | console.log(targetUrl)
76 | } else {
77 | openTarget(targetUrl)
78 | currentTarget.placeholder = ''
79 | }
80 | }
81 | }
82 |
83 | private handleChange({
84 | currentTarget,
85 | }: React.ChangeEvent): void {
86 | return this.props.onChange(currentTarget.value)
87 | }
88 | }
89 |
90 | export function getTargetUrl(
91 | filtered: ItemType[],
92 | value: string,
93 | index: number,
94 | inputValue: string,
95 | ) {
96 | if (index === filtered.length) {
97 | return `${GOOGLE_SEARCH_URL}${inputValue}`
98 | }
99 | if (filtered.length === 0) {
100 | return `${GOOGLE_SEARCH_URL}${value}`
101 | }
102 | return filtered[index].htmlUrl
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/main-view/item/Item.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { ItemType } from '../../../model/item.model'
5 | import {
6 | Text,
7 | // @ts-ignore
8 | } from 'evergreen-ui'
9 |
10 | const DefaultItem = styled.li`
11 | padding: 4px 0px 4px 10px;
12 | height: 100%;
13 | border-bottom: 1px solid #dddddd;
14 | border-left: 3px solid #fafbfc;
15 | vertical-align: middle;
16 | overflow: hidden;
17 | white-space: nowrap;
18 | text-overflow: ellipsis;
19 | `
20 |
21 | const ActiveItem = styled(DefaultItem)`
22 | border-left: 3px solid #0366d6;
23 | background-color: #f3f9ff;
24 | `
25 |
26 | const DetailSpan = styled.span`
27 | padding-left: 4px;
28 | font-size: 12px;
29 | font-weight: lighter;
30 | color: #a9bbcb;
31 | `
32 |
33 | interface ItemProps {
34 | item: ItemType
35 | index: number
36 | curIndex: number
37 | onClickItem: (url: string) => void
38 | }
39 |
40 | export const Item: React.SFC = ({
41 | item,
42 | index,
43 | curIndex,
44 | onClickItem,
45 | }) => {
46 | const { id, name, htmlUrl } = item
47 | const defaultItem = (
48 | onClickItem(htmlUrl)}>
49 |
50 | {name}
51 |
52 |
53 | )
54 | const activeItem = (
55 | onClickItem(htmlUrl)}
59 | >
60 |
61 | {name}
62 | ({htmlUrl})
63 |
64 |
65 | )
66 |
67 | return index === curIndex ? activeItem : defaultItem
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/main-view/itemlist/ItemList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { Item } from '../item/Item'
4 | import { ItemType } from '../../../model/item.model'
5 | import { ItemsLayout } from './ItemsLayout'
6 | import { NotFound } from './not-found/NotFound'
7 | import { $ } from '../../../utils/DomUtils'
8 | import { RepoState } from '../../../reducers/repos.reducers'
9 | import { FetchResponseType } from '../../../saga/repos.saga'
10 | import { Loading } from './loading/Loading'
11 |
12 | interface ItemListProps {
13 | repos: RepoState
14 | onClickItem: (url: string) => void
15 | currentTargetValue: string
16 | }
17 |
18 | const fixScroll = (index: number) => {
19 | const height = 30.9
20 | const boxHeight = 80
21 | const scrollElm: HTMLElement = $('#fix_scroll')
22 | const targetY = index * height - boxHeight
23 |
24 | if (scrollElm) {
25 | scrollElm.scrollTop = targetY
26 | }
27 | }
28 |
29 | export const ItemList: React.SFC = ({
30 | repos,
31 | onClickItem,
32 | currentTargetValue,
33 | }) => {
34 | const { filtered, value, index, fetchResponseType } = repos
35 | const Results = (
36 |
37 | {filtered.map((repo: ItemType, i: number) => (
38 |
45 | ))}
46 | {currentTargetValue && (
47 |
58 | )}
59 |
60 | )
61 | const NoResult =
62 |
63 | fixScroll(index) // FIXME: Currently, Dom select when every render.
64 |
65 | if (fetchResponseType === FetchResponseType.FETCH_READY) {
66 | return
67 | }
68 |
69 | return filtered.length > 0 ? Results : NoResult
70 | }
71 |
--------------------------------------------------------------------------------
/src/components/main-view/itemlist/ItemsLayout.tsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ItemsLayout = styled.ul`
4 | position: relative;
5 | top: 40px;
6 | margin: auto 4px;
7 | padding: 12px;
8 | width: calc(100% - 36px);
9 | height: 100%;
10 | max-height: 180px;
11 | font-size: 14px;
12 | font-weight: bold;
13 | color: #24292e;
14 | background-color: #fafbfc;
15 | border: solid 1px #e2e4e8;
16 | border-radius: 3px;
17 | overflow-y: scroll;
18 | `
19 |
--------------------------------------------------------------------------------
/src/components/main-view/itemlist/loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import { ItemsLayout } from '../ItemsLayout'
5 |
6 | const LoadingElement = styled(ItemsLayout)`
7 | height: 210px;
8 | font-size: 16px;
9 | text-align: center;
10 | `
11 |
12 | const DonutLoading = styled.div`
13 | @keyframes donut-spin {
14 | 0% {
15 | transform: rotate(0deg);
16 | }
17 | 100% {
18 | transform: rotate(360deg);
19 | }
20 | }
21 | display: inline-block;
22 | border: 4px solid rgba(0, 0, 0, 0.1);
23 | border-left-color: #016cd1;
24 | border-radius: 50%;
25 | width: 24px;
26 | height: 24px;
27 | margin-top: 64px;
28 | animation: donut-spin 1.2s linear infinite;
29 | `
30 |
31 | export const Loading: React.SFC = () => (
32 |
33 |
34 |
35 | )
36 |
--------------------------------------------------------------------------------
/src/components/main-view/itemlist/not-found/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import {
4 | Text,
5 | DangerIcon,
6 | // @ts-ignore
7 | } from 'evergreen-ui'
8 |
9 | import { ItemsLayout } from '../ItemsLayout'
10 |
11 | const NotFoundElement = styled(ItemsLayout)`
12 | height: 210px;
13 | font-size: 16px;
14 | text-align: center;
15 | `
16 |
17 | export const NotFound: React.SFC<{ value: string }> = ({ value }) => (
18 |
19 |
20 |
21 |
22 |
23 | We couldn’t find any repositories!
24 |
25 |
26 | If you type enter,
27 | search '{value}' in google.
28 |
29 |
30 | )
31 |
--------------------------------------------------------------------------------
/src/components/setting-view/DomainSetting.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import {
3 | TextInputField,
4 | Popover,
5 | TableRow,
6 | TextTableCell,
7 | Position,
8 | Button,
9 | toaster,
10 | CloseIcon,
11 | // @ts-ignore
12 | } from 'evergreen-ui'
13 | import styled from 'styled-components'
14 | import autobind from 'autobind-decorator'
15 |
16 | import { UserInfoInterface } from '../../service/user-info.service'
17 | import { SettingInfoState } from '../../reducers/setting-info.reducers'
18 | import { RepoState } from '../../reducers/repos.reducers'
19 | import { KeyUtils } from '../../utils/Key'
20 |
21 | const FieldLayout = styled.div`
22 | height: 100%;
23 | `
24 |
25 | const PopupLayout = styled.div`
26 | text-align: center;
27 | margin-top: -12px;
28 | `
29 |
30 | const TableContent = styled.span`
31 | margin-left: 8px;
32 | `
33 |
34 | interface DomainSettingState {
35 | domain: string
36 | }
37 |
38 | interface DomainSettingProps {
39 | repos: RepoState
40 | settingInfo: SettingInfoState
41 | onClickSubmit: (info: UserInfoInterface) => void
42 | onClickClose: () => void
43 | onKeyDown: (domain: string) => void
44 | onClickDelete: (domainInfo: string) => void
45 | }
46 |
47 | export class DomainSetting extends Component<
48 | DomainSettingProps,
49 | DomainSettingState
50 | > {
51 | state: DomainSettingState = {
52 | domain: '',
53 | }
54 |
55 | render() {
56 | return (
57 |
58 |
59 |
67 |
68 | (
70 |
71 |
75 |
76 | {domain}
77 |
78 |
79 | ))}
80 | position={Position.BOTTOM}
81 | >
82 | SHOW FILTERING LIST
83 |
84 |
85 |
86 |
87 | )
88 | }
89 |
90 | @autobind
91 | private handleClickItem({ currentTarget }: React.MouseEvent) {
92 | const target = currentTarget.dataset.value
93 |
94 | if (target) {
95 | this.props.onClickDelete(target)
96 | toaster.success('Deleted!', { duration: 1 })
97 | }
98 | }
99 |
100 | @autobind
101 | private handleKeyDown({
102 | keyCode,
103 | currentTarget,
104 | }: React.KeyboardEvent): void {
105 | if (KeyUtils.isEnterKey(keyCode)) {
106 | const { onKeyDown } = this.props
107 | const { value } = currentTarget
108 |
109 | if (value === '') {
110 | return
111 | }
112 | onKeyDown(value)
113 | currentTarget.value = ''
114 | toaster.success('Success to add', { duration: 1 })
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/setting-view/GitHubSetting.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import {
3 | Button,
4 | TextInputField,
5 | toaster,
6 | // @ts-ignore
7 | } from 'evergreen-ui'
8 | import styled from 'styled-components'
9 | import autobind from 'autobind-decorator'
10 |
11 | import { UserInfoInterface } from '../../service/user-info.service'
12 | import { SettingInfoState } from '../../reducers/setting-info.reducers'
13 | import { RepoState } from '../../reducers/repos.reducers'
14 | import { FetchResponseType } from '../../saga/repos.saga'
15 | import { SETTING_GUIDE_LINK } from '../../main/appConfig'
16 |
17 | const Center = styled.div`
18 | text-align: center;
19 | `
20 |
21 | interface GitHubSettingProps {
22 | repos: RepoState
23 | settingInfo: SettingInfoState
24 | onClickSubmit: (info: UserInfoInterface) => void
25 | onClickClose: () => void
26 | }
27 |
28 | interface GitHubSettingState {
29 | name: string
30 | token: string
31 | }
32 |
33 | export class GitHubSetting extends Component<
34 | GitHubSettingProps,
35 | GitHubSettingState
36 | > {
37 | public state: GitHubSettingState = {
38 | name: '',
39 | token: '',
40 | }
41 |
42 | componentDidMount() {
43 | const { name, token } = this.props.settingInfo.userInfo
44 |
45 | if (name !== '' && token !== '') {
46 | this.setState({ name: name as string, token: token as string })
47 | }
48 | }
49 |
50 | render() {
51 | const { onClickClose, repos, settingInfo } = this.props
52 | const { name: storageName, token: storageToken } = settingInfo.userInfo
53 | const { name, token } = this.state
54 | const { fetchResponseType } = repos
55 | const isDone =
56 | fetchResponseType === FetchResponseType.SUCCESS &&
57 | storageName === name &&
58 | storageToken === token &&
59 | name !== '' &&
60 | token !== ''
61 | let ButtonSection
62 |
63 | if (isDone) {
64 | ButtonSection = (
65 |
66 | Done
67 |
68 | )
69 | } else {
70 | ButtonSection = (
71 |
72 | Submit
73 |
74 | )
75 | }
76 |
77 | return (
78 |
79 |
87 |
96 | {ButtonSection}
97 |
98 | )
99 | }
100 |
101 | @autobind
102 | private handleSubmit() {
103 | const { onClickSubmit } = this.props
104 | const { name, token } = this.state
105 |
106 | if (name !== '' && token !== '') {
107 | onClickSubmit({ name, token })
108 | toaster.success('Complete to connect!', { duration: 1 })
109 | } else {
110 | toaster.warning('Invalid input!', { duration: 1 })
111 | }
112 | }
113 |
114 | @autobind
115 | private handleUpdateValue({ target }: React.ChangeEvent) {
116 | const key = target.dataset.id as keyof GitHubSettingState
117 | const value = target.value
118 |
119 | // @ts-ignore
120 | this.setState({ [key]: value })
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/setting-view/Head.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Heading,
4 | // @ts-ignore
5 | } from 'evergreen-ui'
6 | import styled from 'styled-components'
7 |
8 | const HeadLayout = styled.div`
9 | margin-bottom: 12px;
10 | height: 100%;
11 | `
12 |
13 | export const Head = ({ content }: { content: string }) => (
14 |
15 | {content}
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/src/components/setting-view/SettingView.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import styled from 'styled-components'
3 | import {
4 | CloseIcon,
5 | SegmentedControl,
6 | // @ts-ignore
7 | } from 'evergreen-ui'
8 |
9 | import { Head } from './Head'
10 | import { UserInfoInterface } from '../../service/user-info.service'
11 | import { SettingInfoState } from '../../reducers/setting-info.reducers'
12 | import { RepoState } from '../../reducers/repos.reducers'
13 | import { GitHubSetting } from './GitHubSetting'
14 | import { DomainSetting } from './DomainSetting'
15 |
16 | const SettingViewLayout = styled.div`
17 | position: relative;
18 | margin: 16px;
19 | `
20 |
21 | const IconLayout = styled.div`
22 | position: absolute;
23 | right: -5px;
24 | top: -5px;
25 | `
26 |
27 | const ControllerLayout = styled.div`
28 | margin-bottom: 12px;
29 | `
30 |
31 | interface SettingViewProps {
32 | repos: RepoState
33 | settingInfo: SettingInfoState
34 | onClickSubmit: (info: UserInfoInterface) => void
35 | onClickClose: () => void
36 | onKeyDown: (domainInfo: string) => void
37 | onClickDelete: (domainInfo: string) => void
38 | }
39 |
40 | interface ViewOption {
41 | label: string
42 | value: string
43 | }
44 |
45 | interface SettingViewState {
46 | options: ViewOption[]
47 | viewType: string
48 | }
49 |
50 | export class SettingView extends Component {
51 | state = {
52 | options: [
53 | { label: 'Domain', value: 'domain' },
54 | { label: 'GitHub', value: 'github' },
55 | ],
56 | viewType: 'domain',
57 | }
58 |
59 | handleChangeViewType(selected: string) {
60 | this.setState({
61 | viewType: selected,
62 | })
63 | }
64 |
65 | render(): JSX.Element {
66 | const {
67 | repos,
68 | settingInfo,
69 | onClickSubmit,
70 | onClickClose,
71 | onKeyDown,
72 | onClickDelete,
73 | } = this.props
74 |
75 | const { options, viewType } = this.state
76 |
77 | const GitHubSettingView = (
78 |
84 | )
85 |
86 | const DomainSettingView = (
87 |
95 | )
96 |
97 | return (
98 |
99 |
100 |
101 |
102 |
103 |
104 | this.handleChangeViewType(selected)}
112 | />
113 |
114 | {viewType === 'github' ? GitHubSettingView : DomainSettingView}
115 |
116 | )
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/container/AppContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 |
4 | import { ViewState } from '../reducers/view.reducers'
5 | import MainContainer from './MainContainer'
6 | import SettingContainer from './SettingContainer'
7 |
8 | interface AppContainerProps {
9 | view: ViewState
10 | }
11 |
12 | class AppContainer extends React.Component {
13 | render(): JSX.Element {
14 | const { view } = this.props
15 |
16 | switch (view.type) {
17 | case 'main':
18 | return
19 | case 'setting':
20 | return
21 | default:
22 | return
23 | }
24 | }
25 | }
26 |
27 | const mapStateToProps = (state: AppContainerProps) => ({
28 | view: state.view,
29 | })
30 |
31 | export default connect(mapStateToProps)(AppContainer)
32 |
--------------------------------------------------------------------------------
/src/container/MainContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import autobind from 'autobind-decorator'
4 |
5 | import { RepoState } from '../reducers/repos.reducers'
6 | import { Input } from '../components/main-view/input/Input'
7 | import { ItemList } from '../components/main-view/itemlist/ItemList'
8 | import { Info } from '../components/main-view/info/Info'
9 | import { actions } from '../actions/actions'
10 | import { Dispatch } from 'redux'
11 |
12 | interface MainContainerProps {
13 | repos: RepoState
14 |
15 | decrementIndex: () => void
16 | incrementIndex: () => void
17 | updateValue: (value: string) => void
18 | toggleView: () => void
19 | fetchRequest: () => void
20 | }
21 |
22 | interface MainContainerState {
23 | currentTargetValue: string
24 | }
25 |
26 | class MainContainer extends React.Component<
27 | MainContainerProps,
28 | MainContainerState
29 | > {
30 | state = {
31 | currentTargetValue: '',
32 | }
33 |
34 | componentDidMount() {
35 | this.props.fetchRequest()
36 | }
37 |
38 | @autobind
39 | handlePressUpKey() {
40 | this.props.decrementIndex()
41 | }
42 |
43 | @autobind
44 | handlePressDownKey() {
45 | this.props.incrementIndex()
46 | }
47 |
48 | @autobind
49 | handleInputChange(value: string) {
50 | this.setState(() => ({
51 | currentTargetValue: value,
52 | }))
53 | this.props.updateValue(value)
54 | }
55 |
56 | @autobind
57 | onClickItem(url: string) {
58 | this.openTarget(url)
59 | }
60 |
61 | render(): JSX.Element {
62 | const { currentTargetValue } = this.state
63 | const { repos, toggleView } = this.props
64 |
65 | return (
66 |
67 |
75 |
80 |
81 |
82 | )
83 | }
84 |
85 | private openTarget(url: string) {
86 | chrome.tabs.create({ url })
87 | setTimeout(() => window.close, 300)
88 | }
89 | }
90 |
91 | const mapStateToProps = (state: MainContainerProps) => ({
92 | repos: state.repos,
93 | })
94 |
95 | const mapDispatchToProps = (dispatch: Dispatch) => ({
96 | decrementIndex: () => dispatch(actions.decrementIndex()),
97 | incrementIndex: () => dispatch(actions.incrementIndex()),
98 | updateValue: (value: string) => dispatch(actions.updateValue(value)),
99 | toggleView: () => dispatch(actions.toggleView()),
100 | fetchRequest: () => dispatch(actions.fetchRequest()),
101 | })
102 |
103 | export default connect(
104 | mapStateToProps,
105 | mapDispatchToProps,
106 | )(MainContainer)
107 |
--------------------------------------------------------------------------------
/src/container/SettingContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import autobind from 'autobind-decorator'
4 |
5 | import { SettingView } from '../components/setting-view/SettingView'
6 | import { RepoState } from '../reducers/repos.reducers'
7 | import { SettingInfoState } from '../reducers/setting-info.reducers'
8 | import { UserInfoInterface } from '../service/user-info.service'
9 | import { actions } from '../actions/actions'
10 | import { Dispatch } from 'redux'
11 |
12 | interface SettingContainerProps {
13 | repos: RepoState
14 | settingInfo: SettingInfoState
15 | toggleView: () => void
16 | insertUserInfo: (userInfo: UserInfoInterface) => void
17 | addDomainInfo: (domainInfo: string) => void
18 | deleteDomainInfo: (domainInfo: string) => void
19 | }
20 |
21 | class SettingContainer extends React.Component {
22 | @autobind
23 | handleClickSubmit(userInfo: UserInfoInterface) {
24 | this.props.insertUserInfo(userInfo)
25 | }
26 |
27 | @autobind
28 | handleClickClose() {
29 | this.props.toggleView()
30 | }
31 |
32 | @autobind
33 | handleKeyDown(value: string) {
34 | this.props.addDomainInfo(value)
35 | }
36 |
37 | @autobind
38 | handleClickDelete(target: string) {
39 | this.props.deleteDomainInfo(target)
40 | }
41 |
42 | render(): JSX.Element {
43 | return (
44 |
52 | )
53 | }
54 | }
55 |
56 | const mapStateToProps = (state: SettingContainerProps) => ({
57 | repos: state.repos,
58 | settingInfo: state.settingInfo,
59 | })
60 |
61 | const mapDispatchToProps = (dispatch: Dispatch) => ({
62 | toggleView: () => dispatch(actions.toggleView()),
63 | insertUserInfo: (userInfo: UserInfoInterface) =>
64 | dispatch(actions.insertUserInfo(userInfo)),
65 | addDomainInfo: (domainInfo: string) =>
66 | dispatch(actions.insertDomainInfo(domainInfo)),
67 | deleteDomainInfo: (domainInfo: string) =>
68 | dispatch(actions.deleteDomainInfo(domainInfo)),
69 | })
70 |
71 | export default connect(
72 | mapStateToProps,
73 | mapDispatchToProps,
74 | )(SettingContainer)
75 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { Provider } from 'react-redux'
4 |
5 | import App from './main/App'
6 | import './static/styles/reset.css'
7 |
8 | import configureStore from './store/configureStore'
9 |
10 | const store = configureStore()
11 |
12 | ReactDOM.render(
13 |
14 |
15 | ,
16 | document.getElementById('root') as HTMLElement,
17 | )
18 |
--------------------------------------------------------------------------------
/src/main/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 |
4 | import AppContainer from '../container/AppContainer'
5 |
6 | export const AppLayout = styled.div`
7 | width: 280px;
8 | height: 100%;
9 | `
10 |
11 | export default () => (
12 |
13 |
14 |
15 | )
16 |
--------------------------------------------------------------------------------
/src/main/appConfig.ts:
--------------------------------------------------------------------------------
1 | export const MAX_COUNT_OF_HISTORY: number = 300
2 | export const DEFAULT_FILTERING_URL: string = 'https://github.com/'
3 | export const GOOGLE_SEARCH_URL: string =
4 | 'https://www.google.co.kr/search?q=github: '
5 | export const SETTING_GUIDE_LINK: string = `http://bit.ly/2MucP6G`
6 |
--------------------------------------------------------------------------------
/src/model/item.model.ts:
--------------------------------------------------------------------------------
1 | export type ItemType = {
2 | id: string
3 | name: string
4 | htmlUrl: string
5 | }
6 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { settingInfoReducers } from './setting-info.reducers'
2 | import { combineReducers } from 'redux'
3 | import { reposReducers } from './repos.reducers'
4 | import { viewReducers } from './view.reducers'
5 |
6 | const rootReducers = combineReducers({
7 | repos: reposReducers,
8 | view: viewReducers,
9 | settingInfo: settingInfoReducers,
10 | })
11 |
12 | export default rootReducers
13 |
--------------------------------------------------------------------------------
/src/reducers/repos.reducers.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import parse from 'url-parse'
3 | import { Reducer } from 'redux'
4 | import { ItemType } from '../model/item.model'
5 | import { RepositoryInfo } from '../service/github-repository.service'
6 | import { FetchResponseType } from '../saga/repos.saga'
7 | import { filterByItem } from '../../src/utils/Array'
8 | import { ActionTypes, Actions } from '../actions/actions'
9 |
10 | export interface RepoState {
11 | list: ItemType[]
12 | filtered: ItemType[]
13 | index: number
14 | maxIndex: number
15 | value: string
16 | fetchResponseType: FetchResponseType
17 | }
18 |
19 | const initialState: RepoState = {
20 | list: [],
21 | filtered: [],
22 | index: 0,
23 | value: '',
24 | maxIndex: 0,
25 | fetchResponseType: FetchResponseType.FETCH_READY,
26 | }
27 |
28 | export const reposReducers: Reducer> = (
29 | state: RepoState = initialState,
30 | action: Actions,
31 | ): RepoState => {
32 | switch (action.type) {
33 | case ActionTypes.FETCH_SUCCESS: {
34 | const { response, data } = action.payload
35 | const list = refineData(data)
36 | const refined = _.uniqBy(list, getRepoId)
37 | const filtered = filterByItem(refined, state.value)
38 |
39 | return {
40 | ...state,
41 | list: refined,
42 | filtered,
43 | maxIndex: filtered.length > 0 ? filtered.length : 0, // for search item
44 | fetchResponseType: response,
45 | }
46 | }
47 |
48 | case ActionTypes.FETCH_FAIL: {
49 | return {
50 | ...state,
51 | list: [],
52 | fetchResponseType: FetchResponseType.UNKNOWN_ERROR,
53 | }
54 | }
55 |
56 | case ActionTypes.INCREMENT_INDEX: {
57 | const { index, list } = state
58 | const changedIndex = index + 1
59 |
60 | if (!list[changedIndex]) {
61 | return {
62 | ...state,
63 | index: changedIndex,
64 | }
65 | }
66 |
67 | const selectedValue = list[changedIndex].name
68 |
69 | return {
70 | ...state,
71 | index: changedIndex,
72 | value: selectedValue,
73 | }
74 | }
75 |
76 | case ActionTypes.DECREMENT_INDEX: {
77 | const { index, list } = state
78 | const changedIndex = index - 1
79 | const selectedValue = list[changedIndex].name
80 |
81 | return {
82 | ...state,
83 | index: changedIndex,
84 | value: selectedValue,
85 | }
86 | }
87 |
88 | case ActionTypes.UPDATE_VALUE: {
89 | const value = action.payload
90 | const filtered = filterByItem(state.list, value)
91 |
92 | return {
93 | ...state,
94 | value,
95 | filtered,
96 | index: 0,
97 | maxIndex: filtered.length, // for search item
98 | }
99 | }
100 |
101 | default:
102 | return state
103 | }
104 | }
105 |
106 | export function refineData(rawRepos: RepositoryInfo[]): ItemType[] {
107 | if (!rawRepos) {
108 | return []
109 | }
110 |
111 | return rawRepos
112 | .sort(
113 | ({ url: prevUrl }, { url: nextUrl }) => prevUrl.length - nextUrl.length,
114 | )
115 | .map(({ id, name, url: htmlUrl }: RepositoryInfo) => ({
116 | id,
117 | name,
118 | htmlUrl,
119 | }))
120 | }
121 |
122 | export function getRepoId(item: ItemType): string {
123 | const { htmlUrl: url } = item
124 | const { pathname } = parse(url)
125 |
126 | return pathname
127 | .split('/')
128 | .slice(0, 3)
129 | .join('/')
130 | }
131 |
--------------------------------------------------------------------------------
/src/reducers/setting-info.reducers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | UserInfoInterface,
3 | setUserInfoToLocalStorage,
4 | getUserInfoToLocalStorage,
5 | } from '../service/user-info.service'
6 | import { Actions, ActionTypes } from '../actions/actions'
7 | import { Reducer } from 'redux'
8 | import {
9 | getDomainOptionToLocalStorage,
10 | DomainInfoInterface,
11 | setDomainOptionToLocalStorage,
12 | deleteDomainOptionToLocalStorage,
13 | } from '../service/setting.service'
14 | import { DEFAULT_FILTERING_URL } from '../main/appConfig'
15 |
16 | export interface SettingInfoState {
17 | userInfo: UserInfoInterface
18 | domainInfo: DomainInfoInterface
19 | }
20 |
21 | const userInfoFromStorage = getUserInfoToLocalStorage()
22 | const domainInfoFromStorage = getDomainOptionToLocalStorage()
23 |
24 | const initialState: SettingInfoState = {
25 | userInfo: userInfoFromStorage || { name: '', token: '' },
26 | domainInfo: domainInfoFromStorage || [DEFAULT_FILTERING_URL],
27 | }
28 |
29 | export const settingInfoReducers: Reducer> = (
30 | state: SettingInfoState = initialState,
31 | action: Actions,
32 | ) => {
33 | switch (action.type) {
34 | case ActionTypes.INSERT_USERINFO:
35 | const userInfo = action.payload
36 |
37 | setUserInfoToLocalStorage(userInfo)
38 |
39 | return {
40 | ...state,
41 | ...{
42 | userInfo,
43 | },
44 | }
45 |
46 | case ActionTypes.INSERT_DOMAININFO:
47 | const newDomainInfo = state.domainInfo.concat(action.payload)
48 |
49 | setDomainOptionToLocalStorage(newDomainInfo)
50 |
51 | return {
52 | ...state,
53 | domainInfo: newDomainInfo,
54 | }
55 |
56 | case ActionTypes.DELETE_DOMAININFO:
57 | const updatedDomainInfo = deleteDomainOptionToLocalStorage(action.payload)
58 |
59 | if (!updatedDomainInfo) {
60 | return state
61 | }
62 | return {
63 | ...state,
64 | domainInfo: updatedDomainInfo,
65 | }
66 | default:
67 | return state
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/reducers/view.reducers.ts:
--------------------------------------------------------------------------------
1 | import { Actions, ActionTypes } from '../actions/actions'
2 | import { Reducer } from 'redux'
3 |
4 | type ViewType = 'main' | 'setting'
5 |
6 | export interface ViewState {
7 | type: ViewType
8 | }
9 |
10 | const initialState: ViewState = {
11 | type: 'main',
12 | }
13 |
14 | export const viewReducers: Reducer> = (
15 | state: ViewState = initialState,
16 | action: Actions,
17 | ) => {
18 | // tslint:disable-next-line:no-small-switch
19 | switch (action.type) {
20 | case ActionTypes.TOGGLE_VIEW:
21 | const currentViewType = state.type
22 | const nextViewType = convertViewType(currentViewType)
23 |
24 | return { ...state, ...{ type: nextViewType } }
25 | default:
26 | return state
27 | }
28 | }
29 |
30 | function convertViewType(currentViewType: string): ViewType {
31 | return currentViewType === 'main' ? 'setting' : 'main'
32 | }
33 |
--------------------------------------------------------------------------------
/src/saga/index.ts:
--------------------------------------------------------------------------------
1 | import { all } from 'redux-saga/effects'
2 | import { fetchData, watchFetchRequest } from './repos.saga'
3 |
4 | export default function* rootSaga() {
5 | yield all([fetchData(), watchFetchRequest()])
6 | }
7 |
--------------------------------------------------------------------------------
/src/saga/repos.saga.ts:
--------------------------------------------------------------------------------
1 | import { all, call, put, takeEvery } from 'redux-saga/effects'
2 | import {
3 | fetchGitHubRepository,
4 | RepositoryInfo,
5 | } from '../service/github-repository.service'
6 | import { actions, ActionTypes } from '../actions/actions'
7 | import { getVisitedUrls } from '../service/browser-history.service'
8 |
9 | export enum FetchResponseType {
10 | FETCH_READY = 'FETCH_READY',
11 | SUCCESS = 'SUCCESS',
12 | NOT_AUTHORIZED = 'NOT_AUTHORIZED',
13 | UNKNOWN_ERROR = 'UNKNOWN_ERROR',
14 | }
15 |
16 | export interface FetchDataResponse {
17 | response: FetchResponseType
18 | data: RepositoryInfo[]
19 | message?: string
20 | }
21 |
22 | export function* fetchData(): any {
23 | try {
24 | const [githubRepos, visitedItems] = yield all([
25 | call(fetchGitHubRepository),
26 | call(getVisitedUrls),
27 | ])
28 | const repos = visitedItems.concat(githubRepos)
29 |
30 | if (githubRepos.length === 0) {
31 | yield put(
32 | actions.fetchSuccess({
33 | response: FetchResponseType.NOT_AUTHORIZED,
34 | data: repos,
35 | }),
36 | )
37 | } else {
38 | yield put(
39 | actions.fetchSuccess({
40 | response: FetchResponseType.SUCCESS,
41 | data: repos,
42 | }),
43 | )
44 | }
45 | } catch (error) {
46 | yield put(
47 | actions.fetchFail({
48 | response: FetchResponseType.UNKNOWN_ERROR,
49 | data: [],
50 | }),
51 | )
52 | }
53 | }
54 |
55 | export function* watchFetchRequest() {
56 | yield takeEvery(ActionTypes.FETCH_REQUEST, fetchData)
57 | }
58 |
--------------------------------------------------------------------------------
/src/service/browser-history.service.ts:
--------------------------------------------------------------------------------
1 | import { bookmarks as repoMock } from '../utils/mock/mock.service'
2 | import { RepositoryInfo } from './github-repository.service'
3 | import { HistoryItem } from './browser-history.service'
4 | import { getDomainOptionToLocalStorage } from './setting.service'
5 | import { MAX_COUNT_OF_HISTORY, DEFAULT_FILTERING_URL } from '../main/appConfig'
6 |
7 | export interface HistoryItem {
8 | id: string
9 | url?: string
10 | title?: string
11 | lastVisitTime?: number
12 | typedCount?: number
13 | visitCount?: number
14 | }
15 |
16 | export const getVisitedUrls = async (): Promise => {
17 | if (process.env.NODE_ENV === 'development') {
18 | return repoMock
19 | }
20 | const items = await new Promise((resolve, _) => {
21 | chrome.history.search(
22 | { text: '', maxResults: MAX_COUNT_OF_HISTORY },
23 | (historyItems: HistoryItem[]) => {
24 | resolve(historyItems)
25 | },
26 | )
27 | })
28 | const filteringUrls: string[] = getFilteringUrls()
29 |
30 | return items
31 | .filter((item: HistoryItem) => isFilterItem(item, filteringUrls))
32 | .map(
33 | (item: HistoryItem): RepositoryInfo => ({
34 | id: item.id,
35 | name: item.title as string,
36 | url: item.url as string,
37 | }),
38 | )
39 | }
40 |
41 | function getFilteringUrls(): string[] {
42 | const domainInfo = getDomainOptionToLocalStorage()
43 |
44 | if (domainInfo && domainInfo.length > 0) {
45 | if (domainInfo.indexOf(DEFAULT_FILTERING_URL) >= 0) {
46 | return domainInfo
47 | }
48 | return domainInfo.concat(DEFAULT_FILTERING_URL)
49 | } else {
50 | return [DEFAULT_FILTERING_URL]
51 | }
52 | }
53 |
54 | function isFilterItem(item: HistoryItem, filteringUrls: string[]): boolean {
55 | if (item.title && item.url) {
56 | for (const url of filteringUrls) {
57 | if (item.url.includes(url)) {
58 | return true
59 | }
60 | }
61 | }
62 |
63 | return false
64 | }
65 |
--------------------------------------------------------------------------------
/src/service/github-repository.service.ts:
--------------------------------------------------------------------------------
1 | import { repos as repoMock } from '../utils/mock/mock.service'
2 |
3 | import {
4 | getUserInfoToLocalStorage,
5 | UserInfoInterface,
6 | } from './user-info.service'
7 |
8 | export interface RepositoryInfo {
9 | id: string
10 | name: string
11 | url: string
12 | }
13 |
14 | export const fetchGitHubRepository = async (): Promise => {
15 | if (process.env.NODE_ENV === 'development') {
16 | return repoMock
17 | }
18 | const info = getUserInfoToLocalStorage()
19 |
20 | if (!info) {
21 | return []
22 | }
23 |
24 | const { name, token } = info
25 |
26 | const query = `query {
27 | user(login: ${name}) {
28 | repositories(last: 100) {
29 | edges {
30 | node {
31 | id
32 | name
33 | url
34 | }
35 | }
36 | }
37 | starredRepositories(last: 100) {
38 | edges {
39 | node {
40 | id
41 | name
42 | url
43 | }
44 | }
45 | }
46 | }
47 | }`
48 |
49 | try {
50 | const response = await fetch('https://api.github.com/graphql', {
51 | method: 'POST',
52 | headers: {
53 | 'Content-Type': 'application/json',
54 | Accept: 'application/json',
55 | Authorization: `Bearer ${token}`,
56 | },
57 | body: JSON.stringify({ query }),
58 | })
59 | const json = await response.json()
60 | const userInfo = json.data.user
61 | const userRepositories = userInfo.repositories.edges.map(
62 | ({ node }: { node: RepositoryInfo }) => node,
63 | )
64 | const starredRepositories = userInfo.starredRepositories.edges.map(
65 | ({ node }: { node: RepositoryInfo }) => node,
66 | )
67 |
68 | return userRepositories.concat(starredRepositories)
69 | } catch (e) {
70 | console.error(e)
71 | return []
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/service/setting.service.ts:
--------------------------------------------------------------------------------
1 | import { LocalStorage } from '../storage/LocalStorage'
2 | import { deleteItem } from '../utils/Array'
3 |
4 | const localStorageKey = '#__octodirect_extension_domain_key__#'
5 |
6 | export type DomainInfoInterface = string[]
7 |
8 | export function setDomainOptionToLocalStorage(info: DomainInfoInterface) {
9 | return LocalStorage.setData(localStorageKey, info)
10 | }
11 |
12 | export function getDomainOptionToLocalStorage():
13 | | DomainInfoInterface
14 | | undefined {
15 | const domainInfo = LocalStorage.getData(localStorageKey)
16 |
17 | if (domainInfo) {
18 | return domainInfo
19 | }
20 | return undefined
21 | }
22 |
23 | export function deleteDomainOptionToLocalStorage(
24 | target: string,
25 | ): DomainInfoInterface | undefined {
26 | const domains = getDomainOptionToLocalStorage()
27 |
28 | if (!domains) {
29 | return domains
30 | }
31 |
32 | const updatedDomains = deleteItem(domains, target)
33 |
34 | setDomainOptionToLocalStorage(updatedDomains)
35 |
36 | return updatedDomains
37 | }
38 |
--------------------------------------------------------------------------------
/src/service/user-info.service.ts:
--------------------------------------------------------------------------------
1 | import { LocalStorage } from '../storage/LocalStorage'
2 |
3 | const localStorageKey = '#__octodirect_extension_github_account_key__#'
4 |
5 | export interface UserInfoInterface {
6 | name?: string
7 | token?: string
8 | }
9 |
10 | export function setUserInfoToLocalStorage(info: UserInfoInterface) {
11 | return LocalStorage.setData(localStorageKey, info)
12 | }
13 |
14 | export function getUserInfoToLocalStorage(): UserInfoInterface | undefined {
15 | const userInfo = LocalStorage.getData(localStorageKey)
16 |
17 | if (userInfo) {
18 | return userInfo
19 | }
20 | return undefined
21 | }
22 |
--------------------------------------------------------------------------------
/src/static/images/octodirect_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbee37142/octodirect/13148b07fa1df8c9812e1a940591fabfed7916c8/src/static/images/octodirect_icon.png
--------------------------------------------------------------------------------
/src/static/styles/reset.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 |
90 | /* HTML5 display-role reset for older browsers */
91 |
92 | article,
93 | aside,
94 | details,
95 | figcaption,
96 | figure,
97 | footer,
98 | header,
99 | hgroup,
100 | menu,
101 | nav,
102 | section {
103 | display: block;
104 | }
105 |
106 | body {
107 | line-height: 1;
108 | }
109 |
110 | ol,
111 | ul {
112 | list-style: none;
113 | }
114 |
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 |
120 | blockquote:before,
121 | blockquote:after,
122 | q:before,
123 | q:after {
124 | content: '';
125 | content: none;
126 | }
127 |
128 | table {
129 | border-collapse: collapse;
130 | border-spacing: 0;
131 | }
132 |
133 | input:focus,
134 | select:focus,
135 | textarea:focus,
136 | button:focus {
137 | outline: none;
138 | }
139 |
140 | a {
141 | color: #000;
142 | text-decoration: none;
143 | outline: none
144 | }
145 |
146 | a:hover,
147 | a:active {
148 | text-decoration: none;
149 | color: #000;
150 | background-color: #fff;
151 | }
--------------------------------------------------------------------------------
/src/storage/ChromeStorage.ts:
--------------------------------------------------------------------------------
1 | export enum CHROME_STORAGE_KEY {
2 | GITHUB_TOKEN = '#__octodirect_extension_github_token_key__#',
3 | DOMAIN_KEY_LIST = '#__octodirect_extension_domain_key__#',
4 | }
5 |
6 | export const saveData = (key: CHROME_STORAGE_KEY, value: T) => {
7 | return new Promise((resolve) => {
8 | chrome.storage.sync.set({ [key]: value }, () => resolve(true))
9 | })
10 | }
11 |
12 | export const getData = (key: CHROME_STORAGE_KEY): Promise => {
13 | return new Promise((resolve) => {
14 | chrome.storage.sync.get(key, (items: any) => {
15 | return resolve(items[key])
16 | })
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/src/storage/LocalStorage.ts:
--------------------------------------------------------------------------------
1 | export class LocalStorage {
2 | public static getData(localStorageKey: string): T | undefined {
3 | const rawData = window.localStorage.getItem(localStorageKey)
4 |
5 | if (!rawData) {
6 | return undefined
7 | } else {
8 | return JSON.parse(rawData)
9 | }
10 | }
11 |
12 | public static setData(localStorageKey: string, data: T): void {
13 | window.localStorage.setItem(localStorageKey, JSON.stringify(data))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/store/configureStore.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import reducer from '../reducers'
3 | import saga from '../saga'
4 | import createSagaMiddleware from 'redux-saga'
5 | import logger from 'redux-logger'
6 |
7 | export default function() {
8 | const middlewares = []
9 | const sagaMiddleware = createSagaMiddleware()
10 |
11 | if (process.env.NODE_ENV === 'development') {
12 | middlewares.push(logger)
13 | }
14 | middlewares.push(sagaMiddleware)
15 |
16 | const composeEnhancers =
17 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
18 | const store = createStore(
19 | reducer,
20 | composeEnhancers(applyMiddleware(...middlewares)),
21 | )
22 |
23 | sagaMiddleware.run(saga)
24 |
25 | return store
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/Array.ts:
--------------------------------------------------------------------------------
1 | export function deleteItem(arr: T[], item: T): T[] {
2 | if (!arr || arr.length === 0) {
3 | return arr
4 | }
5 |
6 | const targetIdx = arr.indexOf(item)
7 |
8 | if (targetIdx < 0) {
9 | return arr
10 | }
11 |
12 | if (arr.length === 1) {
13 | return arr.splice(0, targetIdx)
14 | }
15 |
16 | let head: T[] = []
17 | let tail: T[] = []
18 |
19 | if (targetIdx === 0) {
20 | return arr.splice(1, arr.length)
21 | }
22 |
23 | if (targetIdx === 1) {
24 | head = [arr[0]]
25 | } else {
26 | head = arr.slice(0, targetIdx)
27 | }
28 |
29 | if (targetIdx !== arr.length - 1) {
30 | tail = arr.splice(targetIdx + 1, arr.length - 1)
31 | }
32 |
33 | return head.concat(tail)
34 | }
35 |
36 | export function filterByItem(repos: T[], value: string): T[] {
37 | if (value === '') {
38 | return repos
39 | }
40 | return repos.filter((repo: T) =>
41 | (repo as any).name.toLowerCase().includes(value.toLowerCase()),
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/DomUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 전달받은 selector로 HTMLElement를 반환한다.
3 | * @param selector string
4 | */
5 | export const $ = (selector: string): HTMLElement => {
6 | return document.querySelector(selector) as HTMLElement
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/Key.ts:
--------------------------------------------------------------------------------
1 | export enum KEY {
2 | ENTER = 13,
3 | UP = 38,
4 | DOWN = 40,
5 | }
6 |
7 | export const KeyUtils = {
8 | isCorrectUpKey: (keyCode: number, index: number) => {
9 | return keyCode === KEY.UP && index > 0
10 | },
11 |
12 | isCorrectDownKey: (keyCode: number, index: number, maxIndex: number) => {
13 | return keyCode === KEY.DOWN && index < maxIndex
14 | },
15 |
16 | isEnterKey: (keyCode: number) => {
17 | return keyCode === KEY.ENTER
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/src/utils/mock/mock.service.ts:
--------------------------------------------------------------------------------
1 | export const repos = [
2 | {
3 | id: '1',
4 | name: 'testa',
5 | url: 'https://github.com/a/a/issues',
6 | },
7 | {
8 | id: '2',
9 | name: 'testb',
10 | url: 'https://github.com/a/b',
11 | },
12 | {
13 | id: '3',
14 | name: 'testc',
15 | url: 'https://github.com/a/a',
16 | },
17 | {
18 | id: '4',
19 | name: 'testd',
20 | url: 'https://github.com/a/b/issues',
21 | },
22 | {
23 | id: '5',
24 | name: 'teste',
25 | url: 'https://s.search.naver.com/p/around/search.naver',
26 | },
27 | {
28 | id: '6',
29 | name: 'testf',
30 | url: 'https://egghead.io/1',
31 | },
32 | {
33 | id: '7',
34 | name: 'testg',
35 | url: 'https://github.com/',
36 | },
37 | {
38 | id: '8',
39 | name: 'testh',
40 | url: 'https://www.facebook.com/',
41 | },
42 | {
43 | id: '9',
44 | name: 'testi',
45 | url: 'https://twitter.com/',
46 | },
47 | {
48 | id: '10',
49 | name: 'testj',
50 | url: 'http://www.naver.com/',
51 | },
52 | {
53 | id: '11',
54 | name: 'testk',
55 | url: 'https://egghead.io/2',
56 | },
57 | ]
58 |
59 | export const bookmarks = [
60 | {
61 | id: '12',
62 | name: 'bookmarka',
63 | url: 'https://twitter.com/',
64 | },
65 | {
66 | id: '13',
67 | name: 'bookmarkb',
68 | url: 'http://www.naver.com/',
69 | },
70 | {
71 | id: '14',
72 | name: 'bookmarkc',
73 | url: 'https://egghead.io/3',
74 | },
75 | {
76 | id: '15',
77 | name: 'bookmarkc',
78 | url: 'https://egghead.io/4',
79 | },
80 | {
81 | id: '16',
82 | name: 'bookmarkc',
83 | url: 'https://egghead.io/5',
84 | },
85 | {
86 | id: '17',
87 | name: 'bookmarkc',
88 | url: 'https://egghead.io/6',
89 | },
90 | {
91 | id: '18',
92 | name: 'bookmarkc',
93 | url: 'https://egghead.io/7',
94 | },
95 | {
96 | id: '19',
97 | name: 'bookmarkc',
98 | url: 'https://egghead.io/8',
99 | },
100 | {
101 | id: '20',
102 | name: 'bookmarkc',
103 | url: 'https://egghead.io/9',
104 | },
105 | {
106 | id: '21',
107 | name: 'bookmarkc',
108 | url: 'https://egghead.io/',
109 | },
110 | ]
111 |
--------------------------------------------------------------------------------
/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:align */
2 |
3 | export interface CancelableCallbackInterface {
4 | callback: T
5 | cancel(): void
6 | }
7 |
8 | export type AnyFunc = (...args: any[]) => void
9 |
10 | export function throttle(
11 | this: any,
12 | callback: T,
13 | minIntervalMs: number,
14 | ): T {
15 | return throttleWithCancel(callback, minIntervalMs).callback
16 | }
17 |
18 | export function throttleWithCancel(
19 | this: any,
20 | callback: T,
21 | minIntervalMs: number,
22 | ): CancelableCallbackInterface {
23 | let lastEventTime: number = 0
24 | let finalEventTimerId: any = null
25 | const context: any = this
26 |
27 | const func = (...args: any[]) => {
28 | const time = Date.now()
29 | const delta = time - lastEventTime
30 |
31 | if (delta > minIntervalMs) {
32 | lastEventTime = time
33 | callback.apply(context, args)
34 | } else {
35 | if (finalEventTimerId) {
36 | clearTimeout(finalEventTimerId)
37 | finalEventTimerId = null
38 | }
39 | finalEventTimerId = setTimeout(() => {
40 | callback.apply(context, args)
41 | }, minIntervalMs - delta)
42 | }
43 | }
44 |
45 | return {
46 | callback: func as T,
47 | cancel: () => {
48 | clearTimeout(finalEventTimerId)
49 | },
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | install:
5 | - npm install
6 | script:
7 | - npm run test
8 | - npm run build
9 | after_success:
10 | - npm run coveralls
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build/dist",
4 | "module": "esnext",
5 | "target": "es5",
6 | "lib": [
7 | "es6",
8 | "dom"
9 | ],
10 | "sourceMap": true,
11 | "allowJs": false,
12 | "jsx": "preserve",
13 | "esModuleInterop": true,
14 | "moduleResolution": "node",
15 | "forceConsistentCasingInFileNames": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noImplicitAny": true,
19 | "strictNullChecks": true,
20 | "suppressImplicitAnyIndexErrors": true,
21 | "noUnusedLocals": false,
22 | "experimentalDecorators": true,
23 | "skipLibCheck": false,
24 | "allowSyntheticDefaultImports": true,
25 | "strict": true,
26 | "resolveJsonModule": true,
27 | "isolatedModules": true,
28 | "noEmit": true
29 | },
30 | "exclude": [
31 | "node_modules",
32 | "build",
33 | "scripts",
34 | "acceptance-tests",
35 | "webpack",
36 | "jest",
37 | "src/setupTests.ts"
38 | ],
39 | "include": [
40 | "src"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "build/dist",
4 | "module": "commonjs",
5 | "target": "es5",
6 | "lib": ["es6", "dom"],
7 | "sourceMap": true,
8 | "allowJs": false,
9 | "jsx": "react",
10 | "esModuleInterop": true,
11 | "moduleResolution": "node",
12 | "rootDir": "src",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": true,
15 | "noImplicitThis": true,
16 | "noImplicitAny": true,
17 | "strictNullChecks": true,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": true,
20 | "experimentalDecorators": true,
21 | "baseUrl": "."
22 | },
23 | "exclude": [
24 | "node_modules",
25 | "build",
26 | "scripts",
27 | "acceptance-tests",
28 | "webpack",
29 | "jest",
30 | "src/setupTests.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "module": "commonjs"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "tslint-react",
4 | "tslint-config-prettier",
5 | "tslint-sonarts",
6 | "tslint-clean-code"
7 | ],
8 | "linterOptions": {
9 | "exclude": ["**/tests/**"]
10 | },
11 | "rules": {
12 | "align": [true, "parameters", "arguments", "statements"],
13 | "ban": false,
14 | "class-name": true,
15 | "comment-format": [true, "check-space"],
16 | "curly": true,
17 | "eofline": false,
18 | "forin": true,
19 | "indent": [true, "spaces"],
20 | "interface-name": [true, "never-prefix"],
21 | "jsdoc-format": true,
22 | "jsx-no-lambda": false,
23 | "jsx-no-multiline-js": false,
24 | "label-position": true,
25 | "max-line-length": [true, 120],
26 | "member-ordering": [
27 | true,
28 | "public-before-private",
29 | "static-before-instance",
30 | "variables-before-functions"
31 | ],
32 | "no-any": false,
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-console": [
36 | false,
37 | "log",
38 | "error",
39 | "debug",
40 | "info",
41 | "time",
42 | "timeEnd",
43 | "trace"
44 | ],
45 | "no-consecutive-blank-lines": true,
46 | "no-construct": true,
47 | "no-debugger": true,
48 | "no-duplicate-variable": true,
49 | "no-empty": true,
50 | "no-eval": true,
51 | "no-shadowed-variable": true,
52 | "no-string-literal": true,
53 | "no-switch-case-fall-through": true,
54 | "no-trailing-whitespace": false,
55 | "no-unused-expression": true,
56 | "no-use-before-declare": true,
57 | "one-line": [
58 | true,
59 | "check-catch",
60 | "check-else",
61 | "check-open-brace",
62 | "check-whitespace"
63 | ],
64 | "quotemark": [true, "single", "jsx-double"],
65 | "radix": true,
66 | "semicolon": [false, "always"],
67 | "switch-default": true,
68 |
69 | "trailing-comma": [
70 | true,
71 | {
72 | "multiline": {
73 | "objects": "always",
74 | "arrays": "always",
75 | "functions": "always",
76 | "typeLiterals": "ignore"
77 | },
78 | "esSpecCompliant": true
79 | }
80 | ],
81 |
82 | "triple-equals": [true, "allow-null-check"],
83 | "typedef": [true, "parameter", "property-declaration"],
84 | "typedef-whitespace": [
85 | true,
86 | {
87 | "call-signature": "nospace",
88 | "index-signature": "nospace",
89 | "parameter": "nospace",
90 | "property-declaration": "nospace",
91 | "variable-declaration": "nospace"
92 | }
93 | ],
94 | "variable-name": [
95 | true,
96 | "ban-keywords",
97 | "check-format",
98 | "allow-leading-underscore",
99 | "allow-pascal-case"
100 | ],
101 | "whitespace": [
102 | true,
103 | "check-branch",
104 | "check-decl",
105 | "check-module",
106 | "check-operator",
107 | "check-separator",
108 | "check-type",
109 | "check-typecast"
110 | ]
111 | }
112 | }
113 |
--------------------------------------------------------------------------------