├── .editorconfig ├── .gitignore ├── README.md ├── actions └── user.js ├── api └── index.js ├── components ├── app │ ├── corner.vue │ ├── index.vue │ └── style.css ├── computeOften │ ├── index.vue │ └── style.css └── inputBox │ ├── index.vue │ └── style.css ├── css └── vars.css ├── index.html ├── index.js ├── package.json ├── reducers ├── computeOften.js ├── index.js └── loadStatus.js └── store.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # Logs 4 | logs 5 | *.log 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Icon must end with two \r 35 | Icon 36 | 37 | 38 | # Thumbnails 39 | ._* 40 | 41 | # Files that might appear on external disk 42 | .Spotlight-V100 43 | .Trashes 44 | 45 | # Directories potentially created on remote AFP share 46 | .AppleDB 47 | .AppleDesktop 48 | Network Trash Folder 49 | Temporary Items 50 | .apdisk 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # how often ? 2 | 3 | Made with [Tooling](https://github.com/egoist/tooling), [Vue](https://github.com/vuejs/vue) and [Redux](https://github.com/rackt/redux)([Revue](https://github.com/egoist/revue)) http://often.surge.sh/ 4 | 5 | ## Run 6 | 7 | clone the repo and cd to the directory: 8 | 9 | ```bash 10 | npm install 11 | # dev 12 | npm start 13 | # bundle 14 | npm run build 15 | ``` 16 | 17 | ## License 18 | 19 | MIT © [EGOIST](https://github.com/egoist) 20 | -------------------------------------------------------------------------------- /actions/user.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions' 2 | import { fetchUser as fetchUserApi } from '../api' 3 | 4 | export const loading = createAction('loading') 5 | export const fetchUser = createAction('compute often') 6 | export const loaded = createAction('loaded') 7 | export const notFound = createAction('not found') 8 | 9 | export function computeOften(username) { 10 | return async dispatch => { 11 | dispatch(loading()) 12 | const user = await fetchUserApi(username) 13 | if (user.status === 404) { 14 | dispatch(notFound({ username })) 15 | } else { 16 | console.log() 17 | dispatch(fetchUser(await user.json())) 18 | } 19 | dispatch(loaded()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | /* global fetch */ 2 | 3 | export async function fetchUser(username) { 4 | return await fetch(`https://api.github.com/users/${username}`) 5 | } 6 | -------------------------------------------------------------------------------- /components/app/corner.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/app/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /components/app/style.css: -------------------------------------------------------------------------------- 1 | @import '../../css/vars.css'; 2 | 3 | body { 4 | margin: 0; 5 | font: 14px/1.4 "Helvetica Neue", arial, serif; 6 | } 7 | 8 | h1,h2,h3,h4,h5 { 9 | margin: 0; 10 | } 11 | 12 | .app { 13 | .section-a { 14 | background-color: #f9f9f9; 15 | height: 50vmin; 16 | text-align: center; 17 | .sitename { 18 | padding: 100px 0 50px 0; 19 | font-size: 3rem; 20 | a { 21 | color: $linkColor; 22 | text-decoration: none; 23 | } 24 | } 25 | } 26 | .section-b { 27 | height: 50vmin; 28 | text-align: center; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /components/computeOften/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /components/computeOften/style.css: -------------------------------------------------------------------------------- 1 | @import '../../css/vars.css'; 2 | 3 | .compute-often { 4 | font-size: 3rem; 5 | padding-top: 50px; 6 | a { 7 | color: $linkColor; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/inputBox/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 39 | -------------------------------------------------------------------------------- /components/inputBox/style.css: -------------------------------------------------------------------------------- 1 | .input { 2 | border: 1px solid #ccc; 3 | outline: none; 4 | border-radius: 2px; 5 | padding: 8px 12px; 6 | font-size: 1rem; 7 | transition: box-shadow .3s; 8 | background-color: white; 9 | width: 240px; 10 | &:focus { 11 | box-shadow: 0 0 0 3px rgba(48,144,228,.1); 12 | } 13 | } 14 | 15 | .loading { 16 | margin-left: 5px; 17 | } 18 | -------------------------------------------------------------------------------- /css/vars.css: -------------------------------------------------------------------------------- 1 | $linkColor: #2c97e8; 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | manifest="<%=htmlWebpackPlugin.files.manifest %>"<% } %>> 3 | 4 | 5 | 6 | <%=htmlWebpackPlugin.options.title || 'Webpack App'%> 7 | <% if (htmlWebpackPlugin.files.favicon) { %> 8 | 9 | <% } %> 10 | <% for (var css in htmlWebpackPlugin.files.css) { %> 11 | 12 | <% } %> 13 | 14 | 15 | 16 | 17 | <% for (var chunk in htmlWebpackPlugin.files.chunks) { %> 18 | 19 | <% } %> 20 | 21 | 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Revue from 'revue' 3 | import app from './components/app' 4 | import store from './store' 5 | 6 | Vue.use(Revue, { 7 | store 8 | }) 9 | 10 | if (__DEV__) { 11 | Vue.config.debug = true 12 | window.Vue = Vue 13 | window.store = store 14 | } 15 | 16 | new Vue({ 17 | el: 'body', 18 | components: { 19 | app 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "how-often", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npm run clean && tooling watch -e index", 9 | "build": "npm run clean && tooling build -e index", 10 | "clean": "rm -rf ./build", 11 | "release": "npm run build && surge -d often.surge.sh -p ./build" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "tooling": { 16 | "use": "vue", 17 | "index": { 18 | "title": "how often?", 19 | "template": "index.html" 20 | } 21 | }, 22 | "devDependencies": { 23 | "redux": "^3.0.5", 24 | "redux-actions": "^0.9.0", 25 | "redux-thunk": "^1.0.3", 26 | "revue": "^1.2.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /reducers/computeOften.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | const initialState = { 4 | username: null, 5 | often: 0, 6 | found: true 7 | } 8 | 9 | export default handleActions({ 10 | 'not found' (state, action) { 11 | return { 12 | ...state, 13 | ...action.payload, 14 | found: false 15 | } 16 | }, 17 | 'compute often' (state, action) { 18 | const user = action.payload 19 | if (user.created_at) { 20 | let duration = Date.now() - new Date(user.created_at).getTime() 21 | duration = duration / 1000 / 86400 / user.public_repos 22 | const often = user.public_repos ? duration.toFixed(2) : 0 23 | return { 24 | username: user.login, 25 | often, 26 | found: true, 27 | repos: user.public_repos 28 | } 29 | } 30 | return state 31 | } 32 | }, initialState) 33 | -------------------------------------------------------------------------------- /reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import computeOften from './computeOften' 3 | import loadStatus from './loadStatus' 4 | 5 | export default combineReducers({ 6 | computeOften, 7 | loadStatus 8 | }) 9 | -------------------------------------------------------------------------------- /reducers/loadStatus.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from 'redux-actions' 2 | 3 | export default handleActions({ 4 | 'loading' () { 5 | return 'loading' 6 | }, 7 | 'loaded' () { 8 | return 'loaded' 9 | } 10 | }, '') 11 | -------------------------------------------------------------------------------- /store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore, compose } from 'redux' 2 | import rootReducer from './reducers' 3 | import thunk from 'redux-thunk' 4 | 5 | const finalCreateStore = applyMiddleware( 6 | thunk 7 | )(createStore) 8 | const store = finalCreateStore(rootReducer) 9 | 10 | function configureStore () { 11 | if (module.hot) { 12 | module.hot.accept('./reducers', () => { 13 | const nextRootReducer = require('./reducers').default 14 | store.replaceReducer(nextRootReducer) 15 | }) 16 | } 17 | return store 18 | } 19 | 20 | export default configureStore() 21 | --------------------------------------------------------------------------------