├── .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 | octodirect-logo 3 |
4 | 5 | # Octodirect [![extension-version](https://badgen.net/chrome-web-store/v/fmhgcellhhleloebmhbmdncogdddkkag)](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko) [![price](https://badgen.net/chrome-web-store/price/fmhgcellhhleloebmhbmdncogdddkkag)](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko) [![star_of_extension](https://badgen.net/chrome-web-store/stars/fmhgcellhhleloebmhbmdncogdddkkag)](https://chrome.google.com/webstore/detail/octodirect/fmhgcellhhleloebmhbmdncogdddkkag?hl=ko) 6 | 7 | [![Build Status](https://travis-ci.org/JaeYeopHan/octodirect.svg?branch=master)](https://travis-ci.org/JaeYeopHan/octodirect) 8 | [![Coverage Status](https://coveralls.io/repos/github/JaeYeopHan/octodirect/badge.svg?branch=master)](https://coveralls.io/github/JaeYeopHan/octodirect?branch=master) 9 | [![style: styled-components](https://img.shields.io/badge/style-%F0%9F%92%85%20styled--components-orange.svg?colorB=daa357&colorA=db748e)](https://github.com/styled-components/styled-components) 10 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 11 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 12 | ![Contributions welcome](https://img.shields.io/badge/contributions-welcome-purple.svg) 13 | [![License: MIT](https://img.shields.io/packagist/l/doctrine/orm.svg)](https://opensource.org/licenses/MIT) [![Greenkeeper badge](https://badges.greenkeeper.io/JaeYeopHan/octodirect.svg)](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 | 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 | 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 | 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 | 68 | ) 69 | } else { 70 | ButtonSection = ( 71 | 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 | --------------------------------------------------------------------------------