├── .nowignore ├── .npmignore ├── api ├── config.example.json ├── cache.js ├── utils.js └── token.js ├── octolifescreenshot.jpg ├── site ├── public │ ├── github.png │ ├── octolife.jpg │ ├── styles.min.css │ ├── styles.css │ └── app.js ├── index.js ├── authorized.js ├── authorized.html └── page.html ├── README.md ├── .babelrc ├── src ├── utils.js ├── timeline.js ├── piechart.js ├── data.js ├── graphql.js ├── index.js └── ui.js ├── now.json ├── webpack.config.js ├── webpack.prod.js ├── .eslintrc ├── LICENSE ├── package.json └── .gitignore /.nowignore: -------------------------------------------------------------------------------- 1 | api/config.local.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | api/config.local.json 2 | api/config.json -------------------------------------------------------------------------------- /api/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "id": "", 4 | "secret": "" 5 | } 6 | } -------------------------------------------------------------------------------- /octolifescreenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/octolife/HEAD/octolifescreenshot.jpg -------------------------------------------------------------------------------- /site/public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/octolife/HEAD/site/public/github.png -------------------------------------------------------------------------------- /site/public/octolife.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krasimir/octolife/HEAD/site/public/octolife.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Octolife 2 | 3 | The code behind [https://octolife.now.sh/](https://octolife.now.sh/). 4 | 5 | --- 6 | 7 | ![Octolife](./octolifescreenshot.jpg) -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-regenerator", 7 | "@babel/plugin-transform-runtime" 8 | ] 9 | } -------------------------------------------------------------------------------- /site/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const html = fs.readFileSync(`${__dirname}/page.html`); 4 | 5 | module.exports = function(req, res) { 6 | res.setHeader('Content-Type', 'text/html'); 7 | res.end(html); 8 | }; 9 | -------------------------------------------------------------------------------- /site/authorized.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const html = fs.readFileSync(`${__dirname}/authorized.html`); 4 | 5 | module.exports = function(req, res) { 6 | res.setHeader('Content-Type', 'text/html'); 7 | res.end(html); 8 | }; 9 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function diffInDays(date1, date2) { 2 | const diffTime = Math.abs(date2 - date1); 3 | return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); 4 | } 5 | export function formatPlural(value, what) { 6 | if (value === 1) return `${value} ${what}`; 7 | return `${value} ${what}s`; 8 | } 9 | export function getAge(date) { 10 | return formatPlural( 11 | Math.ceil(diffInDays(new Date(), new Date(date)) / 365), 12 | 'year' 13 | ); 14 | } 15 | export function formatHour(hour) { 16 | if (hour < 10) { 17 | return `0${hour}:00`; 18 | } 19 | return `${hour}:00`; 20 | } 21 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { "src": "site/public/*.*", "use": "@now/static" }, 5 | { "src": "site/*.js", "use": "@now/node" }, 6 | { "src": "api/*.*", "use": "@now/node" } 7 | ], 8 | "routes": [ 9 | { "src": "/octolife-api/cache", "dest": "/api/cache.js" }, 10 | { "src": "/octolife-api/token", "dest": "/api/token.js" }, 11 | { "src": "/octolife-api/authorized/(.*)", "dest": "/site/authorized.js" }, 12 | { "src": "/octolife-api/authorized", "dest": "/site/authorized.js" }, 13 | { "src": "/public/(.*)", "dest": "/site/public/$1" }, 14 | { "src": "/(.*)", "dest": "/site/index.js" } 15 | ] 16 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | watch: true, 6 | entry: ['./src/index.js'], 7 | devtool: 'inline-source-map', 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js)$/, 12 | exclude: /node_modules/, 13 | use: ['babel-loader'], 14 | }, 15 | ], 16 | }, 17 | resolve: { 18 | extensions: ['.tsx', '.ts', '.js'], 19 | }, 20 | output: { 21 | path: `${__dirname}/site/public`, 22 | publicPath: '/', 23 | filename: 'app.js', 24 | }, 25 | plugins: [ 26 | new webpack.DefinePlugin({ 27 | __DEV__: true, 28 | }), 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require('terser-webpack-plugin'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: ['./src/index.js'], 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.(js|jsx)$/, 10 | exclude: /node_modules/, 11 | use: ['babel-loader'], 12 | }, 13 | ], 14 | }, 15 | resolve: { 16 | extensions: ['.tsx', '.ts', '.js'], 17 | }, 18 | output: { 19 | path: `${__dirname}/site/public`, 20 | publicPath: '/', 21 | filename: 'app.js', 22 | }, 23 | optimization: { 24 | minimizer: [new TerserPlugin()], 25 | }, 26 | plugins: [ 27 | new webpack.DefinePlugin({ 28 | __DEV__: true, 29 | }), 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "wesbos", 4 | "plugin:@typescript-eslint/eslint-recommended", 5 | "plugin:@typescript-eslint/recommended" 6 | ], 7 | "globals": { 8 | "chrome": true, 9 | "d3": true, 10 | "TimelinesChart": true 11 | }, 12 | "rules": { 13 | "no-restricted-globals": 0, 14 | "react/prop-types": 0, 15 | "react/destructuring-assignment": 0, 16 | "no-plusplus": 0, 17 | "react/jsx-filename-extension": 0, 18 | "@typescript-eslint/no-explicit-any": 0, 19 | "@typescript-eslint/explicit-function-return-type": 0, 20 | "@typescript-eslint/no-var-requires": 0 21 | }, 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [".ts", ".tsx"] 25 | }, 26 | "import/resolver": { 27 | "node": { 28 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 29 | } 30 | } 31 | }, 32 | "parser": "@typescript-eslint/parser", 33 | "plugins": [ 34 | "@typescript-eslint/eslint-plugin" 35 | ] 36 | } -------------------------------------------------------------------------------- /src/timeline.js: -------------------------------------------------------------------------------- 1 | // Lib: https://github.com/vasturiano/timelines-chart 2 | import { formatPlural } from './utils'; 3 | 4 | export default function graph(normalizedRepos, repos, domEl) { 5 | domEl.innerHTML = ` 6 |

${formatPlural( 7 | normalizedRepos.length, 8 | 'repo' 9 | )}, ${formatPlural( 10 | normalizedRepos.reduce((res, repo) => res + repo.totalNumOfCommits, 0), 11 | 'commit' 12 | )}

`; 13 | TimelinesChart()(domEl) 14 | .zScaleLabel('units') 15 | .width(window.innerWidth - 100) 16 | .leftMargin(200) 17 | .rightMargin(10) 18 | .zQualitative(true) 19 | .maxLineHeight(20) 20 | .timeFormat('%Y-%m-%d') 21 | .maxHeight(repos.length * 24) 22 | .zColorScale( 23 | d3.scaleOrdinal( 24 | repos.map(r => r.name), 25 | repos.map(r => `#000`) 26 | ) 27 | ) 28 | .data(normalizedRepos); 29 | 30 | setTimeout(() => { 31 | document.querySelector('.legend').setAttribute('style', 'display: none'); 32 | }, 10); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Krasimir Tsonev 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. 22 | -------------------------------------------------------------------------------- /api/cache.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase, import/no-dynamic-require */ 2 | const { parse } = require("url"); 3 | const { json } = require("micro"); 4 | const demo = require("./demo.json"); 5 | const { error, success } = require("./utils"); 6 | 7 | const CACHE_CONTROL = `s-maxage=${60 * 60 * 24 * 90}`; 8 | 9 | const cache = { 10 | // krasimir: demo, 11 | }; 12 | 13 | module.exports = async (req, res) => { 14 | const { query } = parse(req.url, true); 15 | const { user } = query; 16 | 17 | if (req.method === "OPTIONS") { 18 | return success(res, { hey: "there" }, 201); 19 | } 20 | 21 | if (!user) { 22 | return error(res, "Missing `user` GET param.", 400); 23 | } 24 | 25 | if (req.method === "POST") { 26 | const data = await json(req); 27 | cache[user] = data; 28 | console.log(`Data for "${user}" cached.`); 29 | return success(res, { thanks: "ok" }); 30 | } 31 | 32 | if (cache[user]) { 33 | res.setHeader("Cache-Control", CACHE_CONTROL); 34 | return success(res, { 35 | data: cache[user], 36 | cached: Object.keys(cache), 37 | }); 38 | } 39 | return success(res, { error: "No data" }); 40 | }; 41 | -------------------------------------------------------------------------------- /api/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, import/no-dynamic-require */ 2 | const request = require('superagent'); 3 | 4 | const ENDPOINT = 'https://api.github.com/graphql'; 5 | 6 | export function error(res, error, statusCode = 500) { 7 | res.setHeader('Content-Type', 'application/json'); 8 | res.statusCode = statusCode; 9 | res.end(JSON.stringify({ error: error.message })); 10 | } 11 | export function success(res, data, statusCode = 200) { 12 | res.setHeader('Content-Type', 'application/json'); 13 | res.statusCode = statusCode; 14 | res.end(JSON.stringify(data)); 15 | } 16 | export async function requestGraphQL(query, token) { 17 | console.log(query); 18 | const response = await request 19 | .post(ENDPOINT) 20 | .set('Content-Type', 'application/json') 21 | .set('Authorization', `token ${token}`) 22 | .set('User-Agent', 'Node') 23 | .send({ query }); 24 | 25 | if (response.ok) { 26 | return response.body; 27 | } 28 | throw new Error('Not able to make the request to third party.'); 29 | } 30 | export function getConfig() { 31 | return require(process.env.NODE_ENV === 'development' 32 | ? './config.local.json' 33 | : './config.json'); 34 | } 35 | -------------------------------------------------------------------------------- /site/authorized.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Octolife 7 | 21 | 22 | 23 | 56 | 57 | -------------------------------------------------------------------------------- /api/token.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase, import/no-dynamic-require */ 2 | const { parse } = require('url'); 3 | const request = require('superagent'); 4 | const microCors = require('micro-cors'); 5 | const { error, success, getConfig } = require('./utils'); 6 | 7 | const cors = microCors({ allowMethods: ['GET', 'POST'] }); 8 | 9 | const TOKEN_ENDPOINT = 'https://github.com/login/oauth/access_token'; 10 | const config = getConfig(); 11 | 12 | const getToken = async code => { 13 | const response = await request.post(TOKEN_ENDPOINT).send({ 14 | client_id: config.github.id, 15 | client_secret: config.github.secret, 16 | code, 17 | }); 18 | 19 | if (response.ok) { 20 | const { access_token, error, error_description } = response.body; 21 | 22 | if (error) { 23 | throw new Error(error_description); 24 | } else { 25 | return access_token; 26 | } 27 | } else { 28 | throw new Error('Not able to make the request to third party.'); 29 | } 30 | }; 31 | 32 | module.exports = cors(async (req, res) => { 33 | const { query } = parse(req.url, true); 34 | const { code, redirect, r } = query; 35 | const currentURL = `${req.headers['x-forwarded-proto']}://${req.headers.host}/octolife-api/token?r=${redirect}`; 36 | 37 | if (req.method === 'OPTIONS') { 38 | return success(res, { hey: 'there' }, 201); 39 | } 40 | 41 | // login 42 | if (!code) { 43 | const params = [ 44 | `client_id=${config.github.id}`, 45 | `redirect_uri=${`${currentURL}`}`, 46 | `state=octolife`, 47 | ]; 48 | res.writeHead(301, { 49 | Location: `https://github.com/login/oauth/authorize?${params.join('&')}`, 50 | }); 51 | return res.end(); 52 | } 53 | 54 | // getting the token out of code param 55 | try { 56 | const token = await getToken(code); 57 | res.writeHead(301, { Location: `${r}?t=${token}` }); 58 | return res.end(); 59 | } catch (err) { 60 | console.error(err); 61 | return error(res, err, 403); 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "octolife", 3 | "version": "0.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config ./webpack.prod.js --mode production", 8 | "dev": "concurrently \"now dev\" \"yarn watch\"", 9 | "watch": "webpack --config ./webpack.config.js --mode development", 10 | "release": "yarn build && csso site/public/styles.css --output site/public/styles.min.css && now --prod" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/krasimir/octolife.git" 15 | }, 16 | "author": "Krasimir Tsonev", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/krasimir/octolife/issues" 20 | }, 21 | "homepage": "https://github.com/krasimir/octolife#readme", 22 | "dependencies": { 23 | "lodash": "4.17.15", 24 | "micro": "9.3.4", 25 | "micro-cors": "0.1.1", 26 | "superagent": "5.2.2", 27 | "terser-webpack-plugin": "1.2.3", 28 | "url": "0.11.0", 29 | "webpack": "4.41.6" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "7.5.0", 33 | "@babel/parser": "7.8.8", 34 | "@babel/plugin-transform-regenerator": "7.4.5", 35 | "@babel/plugin-transform-runtime": "7.5.0", 36 | "@babel/preset-env": "7.5.0", 37 | "@babel/runtime": "7.5.0", 38 | "@typescript-eslint/eslint-plugin": "2.19.2", 39 | "@typescript-eslint/parser": "2.19.2", 40 | "babel-eslint": "9.0.0", 41 | "babel-jest": "25.1.0", 42 | "babel-loader": "8.0.4", 43 | "concurrently": "5.1.0", 44 | "csso-cli": "3.0.0", 45 | "eslint": "5.16.0", 46 | "eslint-config-airbnb": "17.1.1", 47 | "eslint-config-prettier": "4.3.0", 48 | "eslint-config-wesbos": "0.0.19", 49 | "eslint-plugin-html": "5.0.5", 50 | "eslint-plugin-import": "2.20.0", 51 | "eslint-plugin-jsx-a11y": "6.2.3", 52 | "eslint-plugin-prettier": "3.1.2", 53 | "eslint-plugin-react": "7.18.0", 54 | "eslint-plugin-react-hooks": "1.7.0", 55 | "prettier": "1.19.1", 56 | "regenerator-runtime": "0.13.2", 57 | "typescript": "3.8.3", 58 | "webpack-cli": "3.1.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/piechart.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | 3 | export default function piechart(data, domEl) { 4 | const width = 300; 5 | const height = 300; 6 | const radius = Math.min(width, height) / 2; 7 | 8 | const color = d3.scaleOrdinal(d3.schemeCategory10); 9 | 10 | const pie = d3.pie().value(function(d) { 11 | return d.value; 12 | }); 13 | 14 | const arc = d3 15 | .arc() 16 | .innerRadius(radius - 100) 17 | .outerRadius(radius - 20); 18 | 19 | const svg = d3 20 | .select(domEl) 21 | .append('svg') 22 | .attr('width', width) 23 | .attr('height', height) 24 | .append('g') 25 | .attr('transform', `translate(${width / 2},${height / 2})`); 26 | 27 | let path = svg 28 | .datum(data) 29 | .selectAll('path') 30 | .data(pie) 31 | .enter() 32 | .append('path') 33 | .attr('fill', function(d, i) { 34 | return d.data.color; 35 | }) 36 | .attr('d', arc) 37 | .each(function(d) { 38 | this._current = d.value; 39 | }); // store the initial angles 40 | 41 | d3.selectAll('input').on('change', change); 42 | 43 | const timeout = setTimeout(function() { 44 | d3.select('input[value="oranges"]') 45 | .property('checked', true) 46 | .each(change); 47 | }, 2000); 48 | 49 | function change() { 50 | const { value } = this; 51 | clearTimeout(timeout); 52 | pie.value(function(d) { 53 | return d[value]; 54 | }); // change the value function 55 | path = path.data(pie); // compute the new angles 56 | path 57 | .transition() 58 | .duration(750) 59 | .attrTween('d', arcTween); // redraw the arcs 60 | } 61 | 62 | function type(d) { 63 | d.apples = +d.apples; 64 | d.oranges = +d.oranges; 65 | return d; 66 | } 67 | 68 | // Store the displayed angles in _current. 69 | // Then, interpolate from _current to the new angles. 70 | // During the transition, _current is updated in-place by d3.interpolate. 71 | function arcTween(a) { 72 | const i = d3.interpolate(this._current, a); 73 | this._current = i(0); 74 | return function(t) { 75 | return arc(i(t)); 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | api/config.local.json 107 | api/config.json 108 | 109 | .vercel -------------------------------------------------------------------------------- /site/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Octolife - GitHub stats 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Octlife

19 |

Your (public) life on GitHub

20 |
21 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 45 | 46 | -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow, @typescript-eslint/no-use-before-define, no-param-reassign */ 2 | import { diffInDays } from './utils'; 3 | 4 | function normalizeDate(str) { 5 | const d = new Date(str); 6 | return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`; 7 | } 8 | 9 | export function getTotalNumOfStars(repos) { 10 | return repos.reduce((res, repo) => res + repo.stargazers.totalCount, 0); 11 | } 12 | 13 | export function getLanguages(repos) { 14 | return repos.reduce((res, repo) => { 15 | repo.languages.nodes.forEach(lang => { 16 | const entry = res.find(e => e.name === lang.name); 17 | if (entry) { 18 | entry.value += 1; 19 | } else { 20 | res.push({ 21 | name: lang.name, 22 | color: lang.color, 23 | value: 1, 24 | }); 25 | } 26 | }); 27 | return res; 28 | }, []); 29 | } 30 | 31 | export function normalizeData(repos, mode = 'all') { 32 | let filterByYear = null; 33 | let filterByLanguage = null; 34 | 35 | if (mode.match(/^year/)) { 36 | filterByYear = Number(mode.replace('year', '')); 37 | } else if (mode.match(/^language_/)) { 38 | filterByLanguage = mode.replace('language_', ''); 39 | } 40 | 41 | const normalizedRepos = repos 42 | .map(repo => { 43 | if (repo.commits.length === 0) return false; 44 | if ( 45 | filterByLanguage && 46 | !repo.languages.nodes.find(l => l.name === filterByLanguage) 47 | ) { 48 | return false; 49 | } 50 | const ranges = []; 51 | const normalizedDates = repo.commits.reduce((r, d) => { 52 | const normalizedDate = normalizeDate(d); 53 | if (!r[normalizedDate]) r[normalizedDate] = 0; 54 | r[normalizedDate] += 1; 55 | return r; 56 | }, {}); 57 | let commitDates = Object.keys(normalizedDates) 58 | .sort((a, b) => new Date(a).getTime() - new Date(b).getTime()) 59 | .map(d => new Date(d)); 60 | 61 | if (filterByYear) { 62 | commitDates = commitDates.filter(d => d.getFullYear() === filterByYear); 63 | } 64 | if (commitDates.length === 0) return false; 65 | 66 | let cursor = commitDates.shift(); 67 | let rangeStart = cursor; 68 | const totalNumOfCommits = Object.keys(normalizedDates).reduce( 69 | (sum, dateStr) => sum + normalizedDates[dateStr], 70 | 0 71 | ); 72 | 73 | commitDates.forEach(d => { 74 | if (diffInDays(d, cursor) > 0) { 75 | ranges.push({ 76 | timeRange: [normalizeDate(rangeStart), normalizeDate(cursor)], 77 | val: repo.name, 78 | }); 79 | rangeStart = d; 80 | } 81 | cursor = d; 82 | }); 83 | 84 | ranges.push({ 85 | timeRange: [normalizeDate(rangeStart), normalizeDate(cursor)], 86 | val: repo.name, 87 | }); 88 | 89 | return { 90 | totalNumOfCommits, 91 | group: repo.name, 92 | data: [{ label: '', data: ranges }], 93 | }; 94 | }) 95 | .filter(v => v) 96 | .sort((a, b) => b.totalNumOfCommits - a.totalNumOfCommits); 97 | return normalizedRepos; 98 | } 99 | -------------------------------------------------------------------------------- /src/graphql.js: -------------------------------------------------------------------------------- 1 | let token = ''; 2 | const endpointGraphQL = 'https://api.github.com/graphql'; 3 | const getHeaders = () => ({ 4 | 'Content-Type': 'application/json', 5 | Authorization: `token ${token}`, 6 | }); 7 | 8 | export const setToken = t => (token = t); 9 | 10 | export const requestGraphQL = async function(query, customHeaders = {}) { 11 | try { 12 | const res = await fetch(endpointGraphQL, { 13 | headers: Object.assign({}, getHeaders(), customHeaders), 14 | method: 'POST', 15 | body: JSON.stringify({ query }), 16 | }); 17 | 18 | if (!res.ok) { 19 | throw new Error(`${res.status} ${res.statusText}`); 20 | } 21 | // console.log(`Rate limit remaining: ${res.headers.get('x-ratelimit-remaining')}`); 22 | const resultData = await res.json(); 23 | 24 | if (resultData.errors) { 25 | console.warn(`There are errors while requesting ${endpointGraphQL}`); 26 | console.warn(resultData.errors.map(({ message }) => message)); 27 | } 28 | return resultData; 29 | } catch (err) { 30 | if (err.toString().match(/401 Unauthorized/)) { 31 | localStorage.removeItem('OCTOLIFE_GH_TOKEN'); 32 | window.location.href = '/'; 33 | } else { 34 | document.querySelector('#root').innerHTML = ` 35 |
36 |

Error

37 |

Please try again in a few minutes.

38 |
39 | `; 40 | } 41 | } 42 | }; 43 | 44 | export const QUERY_GET_REPOS = (login, cursor) => ` 45 | query { 46 | search(query: "user:${login}", type: REPOSITORY, first: 100${ 47 | cursor ? `, after: "${cursor}"` : '' 48 | }) { 49 | repositoryCount, 50 | edges { 51 | cursor, 52 | node { 53 | ... on Repository { 54 | name, 55 | createdAt, 56 | descriptionHTML, 57 | diskUsage, 58 | forkCount, 59 | homepageUrl, 60 | stargazers { 61 | totalCount 62 | }, 63 | issues(states: OPEN) { 64 | totalCount 65 | }, 66 | languages(first:15) { 67 | nodes { 68 | name, 69 | color 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | `; 78 | 79 | export const QUERY_GET_COMMITS = (user, repo, cursor) => ` 80 | { 81 | repository(owner: "${user}", name: "${repo}") { 82 | object(expression: "master") { 83 | ... on Commit { 84 | history(first: 100${cursor ? `, after: "${cursor}"` : ''}) { 85 | nodes { 86 | committedDate 87 | } 88 | pageInfo { 89 | endCursor 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | `; 97 | 98 | export const QUERY_USER = user => ` 99 | query { 100 | search(query: "user:${user}", type: USER, first: 1) { 101 | userCount, 102 | edges { 103 | node { 104 | ... on User { 105 | name, 106 | login, 107 | avatarUrl, 108 | bio, 109 | company, 110 | createdAt, 111 | location, 112 | url, 113 | websiteUrl, 114 | followers { 115 | totalCount 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | `; 123 | -------------------------------------------------------------------------------- /site/public/styles.min.css: -------------------------------------------------------------------------------- 1 | *{box-sizing:border-box;word-break:break-word}body{width:100%;height:100%;font-size:20px;line-height:28px}a{color:#184706}a:hover{color:#7ab80a}h1{margin:0;font-family:'Abril Fatface',cursive;font-size:5em}h1,h2,h3{text-align:center}body,h1,h2,h3,h4,p{padding:0}body,h2,h3,h4{font-family:'Source Sans Pro',sans-serif;margin:0}h4{text-align:left}@media all and (max-width:450px){h1{font-size:3em}}h1 img{width:.8em;height:.8em;display:inline-block;margin:0 .1em;transform:translate(0,2px)}.form>p,footer,hr{max-width:600px}hr{margin:3em auto;border-top:none;border-bottom:dotted 1px #b0b0b0}p{margin:0 0 1.4em}ul{list-style:none}.report .user h2,ul,ul li{margin:0;padding:0}.tac{text-align:center!important}.right{float:right}.clear{clear:both}.m0{margin:0!important}.mt2{margin-top:2em!important}.mb2,.my2{margin-bottom:2em!important}.my2{margin-top:2em!important}.my1{margin-bottom:1em!important}.mt1,.my1{margin-top:1em!important}.mb1{margin-bottom:1em!important}.mt05{margin-top:.5em!important}.o05{opacity:.5!important}.block{display:block!important}.inline{display:inline-block!important}.grid2{max-width:940px;display:grid;grid-template-columns:1fr 1fr;margin:0 auto;column-gap:1em}.grid2.bordered{border-top:solid 1px #b0b0b0;padding-top:1em}@media all and (max-width:940px){.grid2{display:block}.grid2>div{padding:0 1.4em}}.emoji,.report h1{display:inline-block}.emoji{transform:translate(0,3px)}.authorize,.report select{display:block;margin:0 auto}.authorize{text-decoration:none;max-width:350px;color:#fff;background:#58950e;border-radius:6px;text-align:center;transition:background 200ms ease-out;border-bottom:solid 3px #355a09;border-right:solid 2px #355a09}.authorize:hover{color:#fff;background:#284308}.form>p{margin-left:auto;margin-right:auto}.form input{display:block;font-size:1em;padding:.4em;border-radius:4px;width:100%}#how-to-get-token{display:none}.report h1{text-align:left;margin:0 .5em 0 0;padding:0 .5em 0 0;font-size:1.3em;border-right:solid 1px #999}.report header{padding:1em;border-bottom:dotted 1px #999;margin-bottom:1em}.report header h1 a{color:#000;text-decoration:none}.report .header a{display:inline-block;margin-left:.6em}.authorize,.report .user{padding:1em}.report .user h2{font-size:3em;text-align:center;line-height:1em}.report .user h2 img{display:block;width:120px;height:120px;border-radius:67% 33% 35% 65%/38% 30% 70% 62%;margin:0 auto .5em}.lines{line-height:1em}.lines .lang-item{clear:both;height:25px}.lines .lang-item span{display:block;font-size:.7em;border-radius:2px;float:left;color:#fff}.lines .lang-item small{display:block;float:left;margin-left:.2em}.report h3{font-size:2em}.report select{line-height:1.3;font-size:.8em;padding:.6em 1.4em .5em .8em;width:200px;border:1px solid #aaa;box-shadow:0 1px 0 1px rgba(0,0,0,.04);border-radius:.5em;-moz-appearance:none;-webkit-appearance:none;appearance:none;background-color:#fff;background-image:url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E),linear-gradient(to bottom,#fff 0,#e5e5e5 100%);background-repeat:no-repeat,repeat;background-position:right .7em top 50%,0 0;background-size:.65em auto,100%}footer{padding-top:2em;border-top:dotted 1px #b0b0b0;margin:3em auto 4em}footer p{max-width:250px;margin:1em auto} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | import { parse } from 'url'; 3 | import get from 'lodash/get'; 4 | import { 5 | setToken, 6 | QUERY_GET_REPOS, 7 | requestGraphQL, 8 | QUERY_GET_COMMITS, 9 | QUERY_USER, 10 | } from './graphql'; 11 | 12 | import UI from './ui'; 13 | 14 | const CACHE_CONTROL = `s-maxage=${60 * 60 * 24 * 90}`; 15 | 16 | async function getRepos(login) { 17 | let cursor; 18 | let repos = []; 19 | const getRepo = async function() { 20 | const q = QUERY_GET_REPOS(login, cursor); 21 | const { data } = await requestGraphQL(q); 22 | 23 | repos = repos.concat(data.search.edges); 24 | 25 | if (data.search.repositoryCount > repos.length) { 26 | cursor = repos[repos.length - 1].cursor.replace('==', ''); 27 | return getRepo(); 28 | } 29 | return repos.map(r => r.node); 30 | }; 31 | 32 | return getRepo(); 33 | } 34 | async function getRepoCommits(user, repoName) { 35 | const perPage = 100; 36 | let cursor; 37 | let commits = []; 38 | const getCommits = async function() { 39 | const q = QUERY_GET_COMMITS(user, repoName, cursor); 40 | const { data } = await requestGraphQL(q); 41 | 42 | commits = commits.concat(get(data, 'repository.object.history.nodes')); 43 | 44 | const endCursor = get(data, 'repository.object.history.pageInfo.endCursor'); 45 | if (endCursor) { 46 | cursor = endCursor; 47 | return getCommits(); 48 | } 49 | return commits 50 | .map(d => (d ? d.committedDate || false : false)) 51 | .filter(v => v); 52 | }; 53 | 54 | return getCommits(); 55 | } 56 | async function getUser(profileName) { 57 | const { data } = await requestGraphQL(QUERY_USER(profileName)); 58 | return get(data, 'search.edges.0.node', null); 59 | } 60 | async function annotateReposWithCommitDates(user, repos, log) { 61 | let repoIndex = 0; 62 | async function annotate() { 63 | if (repoIndex >= repos.length) { 64 | return; 65 | } 66 | const repo = repos[repoIndex]; 67 | const percent = Math.ceil((repoIndex / repos.length) * 100); 68 | log(`⌛ Getting commit history (${percent}%)`, true); 69 | repo.commits = await getRepoCommits(user, repo.name); 70 | repoIndex += 1; 71 | await annotate(); 72 | } 73 | await annotate(); 74 | } 75 | 76 | async function cacheData(user, repos) { 77 | try { 78 | const res = await fetch(`/octolife-api/cache?user=${user.login}`, { 79 | method: 'POST', 80 | headers: { 81 | 'Content-Type': 'application/json', 82 | }, 83 | body: JSON.stringify({ user, repos }), 84 | }); 85 | console.log(`Cache: ${JSON.stringify(await res.json())}`); 86 | // This call is to force Zeit to cache the api call. That is because 87 | // Zeit is not caching lambda responses when we have POST request. 88 | // We need a GET request. That's why also api/cache.js has some memory cache. 89 | // The data needs to be there in order to get it served back and cached by the CDN. 90 | getCacheData(user.login); 91 | } catch (err) { 92 | console.log(err); 93 | } 94 | } 95 | 96 | async function getCacheData(username) { 97 | try { 98 | const res = await fetch(`/octolife-api/cache?user=${username}`, { 99 | method: 'GET', 100 | headers: { 101 | 'Content-Type': 'application/json', 102 | 'Cache-Control': CACHE_CONTROL, 103 | }, 104 | }); 105 | const d = await res.json(); 106 | if (d.data) { 107 | return d.data; 108 | } 109 | return false; 110 | } catch (err) { 111 | return false; 112 | } 113 | } 114 | 115 | window.addEventListener('load', async function() { 116 | const { 117 | renderLoading, 118 | renderLoader, 119 | renderProfileRequiredForm, 120 | renderReport, 121 | renderTokenRequiredForm, 122 | } = UI(); 123 | let profileNameFromTheURL = parse(window.location.href) 124 | .path.replace(/^\//, '') 125 | .split('/') 126 | .shift(); 127 | if (profileNameFromTheURL.match(/^\?/)) { 128 | profileNameFromTheURL = ''; 129 | } 130 | const token = localStorage.getItem('OCTOLIFE_GH_TOKEN'); 131 | setToken(token); 132 | 133 | async function useCacheData(profileName) { 134 | renderLoading(`⌛ Loading. Please wait.`); 135 | const cachedData = await getCacheData(profileName); 136 | if (cachedData && cachedData.user && cachedData.repos) { 137 | renderReport(cachedData.user, cachedData.repos); 138 | return true; 139 | } 140 | return false; 141 | } 142 | async function fetchProfile(profileName) { 143 | if (!token) { 144 | renderTokenRequiredForm(profileName); 145 | return; 146 | } 147 | const log = renderLoader(); 148 | log('⌛ Getting profile information ...'); 149 | const user = await getUser(profileName); 150 | if (user === null) { 151 | renderProfileRequiredForm( 152 | fetchProfile, 153 | `⚠️ There is no user with profile name "${profileName}". Try again.` 154 | ); 155 | } else { 156 | log(`✅ Profile information.`, true); 157 | log(`⌛ Getting ${user.name}'s repositories ...`); 158 | const repos = await getRepos(user.login); 159 | log(`✅ ${user.name}'s repositories.`, true); 160 | log(`⌛ Getting commit history ...`); 161 | await annotateReposWithCommitDates(user.login, repos, log); 162 | log(`✅ Commits.`, true); 163 | cacheData(user, repos); 164 | renderReport(user, repos); 165 | } 166 | } 167 | 168 | if (profileNameFromTheURL !== '') { 169 | if (await useCacheData(profileNameFromTheURL)) { 170 | return; 171 | } 172 | fetchProfile(profileNameFromTheURL); 173 | } else { 174 | renderProfileRequiredForm(async profileName => { 175 | if (await useCacheData(profileName)) { 176 | return; 177 | } 178 | fetchProfile(profileName); 179 | }); 180 | } 181 | }); 182 | -------------------------------------------------------------------------------- /site/public/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | word-break: break-word; 4 | } 5 | body { 6 | width: 100%; 7 | height: 100%; 8 | font-family: 'Source Sans Pro', sans-serif; 9 | font-size: 20px; 10 | line-height: 28px; 11 | margin: 0; 12 | padding: 0; 13 | } 14 | a { 15 | color: #184706; 16 | } 17 | a:hover { 18 | color: #7ab80a; 19 | } 20 | h1 { 21 | margin: 0; 22 | padding: 0; 23 | font-family: 'Abril Fatface', cursive; 24 | text-align: center; 25 | } 26 | h2, h3, h4 { 27 | font-family: 'Source Sans Pro', sans-serif; 28 | margin: 0; 29 | padding: 0; 30 | text-align: center; 31 | } 32 | h1 { 33 | font-size: 5em; 34 | } 35 | @media all and (max-width: 450px) { 36 | h1 { 37 | font-size: 3em; 38 | } 39 | } 40 | h1 img { 41 | width: 0.8em; 42 | height: 0.8em; 43 | display: inline-block; 44 | margin: 0 0.1em; 45 | transform: translate(0, 2px); 46 | } 47 | h4 { 48 | text-align: left; 49 | } 50 | hr { 51 | margin: 3em auto; 52 | border-top: none; 53 | border-bottom: dotted 1px #B0B0B0; 54 | max-width: 600px; 55 | } 56 | p { 57 | margin: 0 0 1.4em 0; 58 | padding: 0; 59 | } 60 | ul { 61 | list-style: none; 62 | margin: 0; 63 | padding: 0; 64 | } 65 | ul li { 66 | margin: 0; 67 | padding: 0; 68 | } 69 | .tac { 70 | text-align: center !important; 71 | } 72 | .right { 73 | float: right; 74 | } 75 | .clear { 76 | clear: both; 77 | } 78 | .m0 { 79 | margin: 0 !important; 80 | } 81 | .mt2 { 82 | margin-top: 2em !important; 83 | } 84 | .mb2 { 85 | margin-bottom: 2em !important; 86 | } 87 | .my2 { 88 | margin-top: 2em !important; 89 | margin-bottom: 2em !important; 90 | } 91 | .my1 { 92 | margin-top: 1em !important; 93 | margin-bottom: 1em !important; 94 | } 95 | .mt1 { 96 | margin-top: 1em !important; 97 | } 98 | .mb1 { 99 | margin-bottom: 1em !important; 100 | } 101 | .mt05 { 102 | margin-top: 0.5em !important; 103 | } 104 | .o05 { 105 | opacity: 0.5 !important; 106 | } 107 | .block { 108 | display: block !important; 109 | } 110 | .inline { 111 | display: inline-block !important; 112 | } 113 | .grid2 { 114 | max-width: 940px; 115 | display: grid; 116 | grid-template-columns: 1fr 1fr; 117 | margin: 0 auto; 118 | column-gap: 1em; 119 | } 120 | .grid2.bordered { 121 | border-top: solid 1px #B0B0B0; 122 | padding-top: 1em; 123 | } 124 | @media all and (max-width: 940px) { 125 | .grid2 { 126 | display: block; 127 | } 128 | .grid2 > div { 129 | padding: 0 1.4em; 130 | } 131 | } 132 | .emoji { 133 | display: inline-block; 134 | transform: translate(0, 3px); 135 | } 136 | 137 | .authorize { 138 | display: block; 139 | text-decoration: none; 140 | max-width: 350px; 141 | margin: 0 auto; 142 | color: #fff; 143 | background: #58950e; 144 | border-radius: 6px; 145 | padding: 1em; 146 | text-align: center; 147 | transition: background 200ms ease-out; 148 | border-bottom: solid 3px #355a09; 149 | border-right: solid 2px #355a09; 150 | } 151 | .authorize:hover { 152 | color: #fff; 153 | background: #284308; 154 | } 155 | .form > p { 156 | margin-left: auto; 157 | margin-right: auto; 158 | max-width: 600px; 159 | } 160 | .form input { 161 | display: block; 162 | font-size: 1em; 163 | padding: 0.4em; 164 | border-radius: 4px; 165 | width: 100%; 166 | } 167 | #how-to-get-token { 168 | display: none; 169 | } 170 | 171 | /* Report */ 172 | 173 | .report h1 { 174 | text-align: left; 175 | margin: 0 0.5em 0 0; 176 | padding: 0 0.5em 0 0; 177 | font-size: 1.3em; 178 | display: inline-block; 179 | border-right: solid 1px #999; 180 | } 181 | .report header { 182 | padding: 1em; 183 | border-bottom: dotted 1px #999; 184 | margin-bottom: 1em; 185 | } 186 | .report header h1 a { 187 | color: #000; 188 | text-decoration: none; 189 | } 190 | .report .header a { 191 | display: inline-block; 192 | margin-left: 0.6em; 193 | } 194 | .report .user { 195 | padding: 1em; 196 | } 197 | .report .user h2 { 198 | padding: 0; 199 | margin: 0; 200 | font-size: 3em; 201 | text-align: center; 202 | line-height: 1em; 203 | } 204 | .report .user h2 img { 205 | display: block; 206 | width: 120px; 207 | height: 120px; 208 | border-radius: 67% 33% 35% 65% / 38% 30% 70% 62%; 209 | margin: 0 auto 0.5em auto; 210 | } 211 | .lines { 212 | line-height: 1em; 213 | } 214 | .lines .lang-item { 215 | clear: both; 216 | height: 25px; 217 | } 218 | .lines .lang-item span { 219 | display: block; 220 | font-size: 0.7em; 221 | border-radius: 2px; 222 | float: left; 223 | color: #fff 224 | } 225 | .lines .lang-item small { 226 | display: block; 227 | float: left; 228 | margin-left: 0.2em; 229 | } 230 | .report h3 { 231 | font-size: 2em; 232 | } 233 | .report select { 234 | display: block; 235 | line-height: 1.3; 236 | font-size: 0.8em; 237 | padding: .6em 1.4em .5em .8em; 238 | width: 200px; 239 | margin: 0 auto; 240 | border: 1px solid #aaa; 241 | box-shadow: 0 1px 0 1px rgba(0,0,0,.04); 242 | border-radius: .5em; 243 | -moz-appearance: none; 244 | -webkit-appearance: none; 245 | appearance: none; 246 | background-color: #fff; 247 | background-image: url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E), linear-gradient(to bottom, #ffffff 0%,#e5e5e5 100%); 248 | background-repeat: no-repeat, repeat; 249 | background-position: right .7em top 50%, 0 0; 250 | background-size: .65em auto, 100%; 251 | } 252 | footer { 253 | padding-top: 2em; 254 | border-top: dotted 1px #B0B0B0; 255 | margin: 3em auto 4em auto; 256 | max-width: 600px; 257 | } 258 | footer p { 259 | max-width: 250px; 260 | margin: 1em auto; 261 | } -------------------------------------------------------------------------------- /src/ui.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow, @typescript-eslint/no-use-before-define, no-param-reassign */ 2 | import timeline from './timeline'; 3 | import piechart from './piechart'; 4 | import { normalizeData, getTotalNumOfStars, getLanguages } from './data'; 5 | import { getAge, formatPlural, formatHour } from './utils'; 6 | 7 | const $ = sel => document.querySelector(sel); 8 | const weekDaysMap = [ 9 | 'Sunday', 10 | 'Monday', 11 | 'Tuesday', 12 | 'Wednesday', 13 | 'Thursday', 14 | 'Friday', 15 | 'Saturday', 16 | ]; 17 | 18 | function renderHeader() { 19 | return ` 20 |

Octlife

21 |

Your (public) life on GitHub

22 | `; 23 | } 24 | 25 | function renderLoading(message) { 26 | $('#root').innerHTML = ` 27 |
28 | ${renderHeader()} 29 |
30 |

31 | ${message} 32 |

33 |
34 | `; 35 | } 36 | 37 | function renderTokenRequiredForm(profileNameFromTheURL) { 38 | $('#root').innerHTML = ` 39 |
40 | ${renderHeader()} 41 |
42 |

43 | Authorize Octolife GitHub app
to see the report
44 |

45 |
46 |

📌 Octolife caches your profile for 90 days so other users can see it without authorization.

47 |
48 | `; 49 | } 50 | 51 | function renderProfileRequiredForm(profileNameProvided, message) { 52 | $('#root').innerHTML = ` 53 |
54 | ${renderHeader()} 55 |
56 | ${message ? `

${message}

` : ''} 57 |

58 | 59 | Enter a GitHub profile name and hit Enter. 60 |

61 |
62 |

🤔 Wonder how an Octolife report looks like? Go check one here.

63 |
64 | `; 65 | const input = $('#github-profile'); 66 | input.addEventListener('keyup', function(e) { 67 | if (e.keyCode === 13) { 68 | const token = input.value; 69 | if (token === '') { 70 | input.style['outline-color'] = 'red'; 71 | } else { 72 | profileNameProvided(token); 73 | } 74 | } 75 | }); 76 | setTimeout(() => { 77 | input.focus(); 78 | }, 20); 79 | } 80 | 81 | function renderLoader() { 82 | $('#root').innerHTML = ` 83 |
84 | ${renderHeader()} 85 |
86 |

87 |
88 | `; 89 | const content = $('#loader-content'); 90 | const logs = []; 91 | return (str, replaceLastLog = false) => { 92 | if (!replaceLastLog) { 93 | logs.push(str); 94 | } else { 95 | logs[logs.length - 1] = str; 96 | } 97 | content.innerHTML = logs.map(s => `
${s}
`).join(''); 98 | }; 99 | } 100 | 101 | function renderReport(user, repos) { 102 | history.pushState({}, `Octolife / ${user.name}`, `/${user.login}`); 103 | 104 | const languages = getLanguages(repos).sort((a, b) => b.value - a.value); 105 | const languagesTotal = languages.reduce((res, lang) => res + lang.value, 0); 106 | const years = repos 107 | .reduce((res, repo) => { 108 | repo.commits.forEach(commitDate => { 109 | const year = new Date(commitDate).getFullYear(); 110 | if (!res.includes(year)) res.push(year); 111 | }); 112 | return res; 113 | }, []) 114 | .sort((a, b) => a - b); 115 | const weekDays = repos.reduce((res, repo) => { 116 | repo.commits.forEach(commit => { 117 | const day = weekDaysMap[new Date(commit).getDay()]; 118 | if (typeof res[day] === 'undefined') res[day] = 0; 119 | res[day] += 1; 120 | }); 121 | return res; 122 | }, {}); 123 | const weekDaysTotal = weekDaysMap.reduce( 124 | (res, day) => res + (weekDays[day] || 0), 125 | 0 126 | ); 127 | const hours = repos.reduce((res, repo) => { 128 | repo.commits.forEach(commit => { 129 | const hour = new Date(commit).getHours(); 130 | if (typeof res[hour] === 'undefined') res[hour] = 0; 131 | res[hour] += 1; 132 | }); 133 | return res; 134 | }, {}); 135 | const hoursTotal = Object.keys(hours).reduce( 136 | (res, hour) => res + (hours[hour] || 0), 137 | 0 138 | ); 139 | 140 | $('#root').innerHTML = ` 141 |
142 |
143 |

144 | Octgithublife 145 |

146 | New Report 147 |
148 |
149 |

${user.name}${user.name}

150 |

🌟 ${formatPlural( 151 | getTotalNumOfStars(repos), 152 | 'star' 153 | )}

154 |
155 |
156 |
157 |
    158 |
  • @GitHub: ${user.url}
  • 161 | ${ 162 | user.websiteUrl 163 | ? `
  • @Web: ${user.websiteUrl}
  • ` 164 | : '' 165 | } 166 |
  • Age: ${getAge(user.createdAt)}
  • 167 | ${ 168 | user.location 169 | ? `
  • Location: ${user.location}
  • ` 170 | : '' 171 | } 172 | ${ 173 | user.company 174 | ? `
  • Location: ${user.company}
  • ` 175 | : '' 176 | } 177 |
  • Repositories: ${repos.length}
  • 178 |
  • Followers: ${user.followers.totalCount}
  • 179 |
180 |
181 |
182 | ${user.bio ? `

${user.bio}

` : ''} 183 | ${ 184 | user.pinnedRepositories && user.pinnedRepositories.nodes.length > 0 185 | ? `

Pins: ${user.pinnedRepositories.nodes 186 | .map( 187 | r => 188 | `${r.name}(★${r.stargazers.totalCount})` 189 | ) 190 | .join(', ')}

` 191 | : '' 192 | } 193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
${languages 202 | .map(lang => { 203 | const percent = ((lang.value / languagesTotal) * 100).toFixed(1); 204 | return `
205 |   207 | ${percent}% ${lang.name} 208 |
`; 209 | }) 210 | .join('')}
211 |
212 |
213 |
214 |

Time

215 |
216 |
217 |
${weekDaysMap 218 | .map(day => { 219 | const perc = ((weekDays[day] / weekDaysTotal) * 100).toFixed(1); 220 | return `
221 |   223 | ${perc}%   ${day} 224 |
`; 225 | }) 226 | .join('')}
227 |
228 | ${Object.keys(hours) 229 | .map(hour => { 230 | const perc = ((hours[hour] / hoursTotal) * 100).toFixed(1); 231 | return `
232 |   234 | ${perc}%   ${formatHour( 235 | hour 236 | )} 237 |
`; 238 | }) 239 | .join('')} 240 |
241 |
242 |
243 |
244 |

Timeline (commit history)

245 |
246 | 255 |
256 |
257 |
258 |

Repositories

259 |
260 | ${repos 261 | .sort((a, b) => b.stargazers.totalCount - a.stargazers.totalCount) 262 | .map(repo => { 263 | const props = [ 264 | `${formatPlural(repo.stargazers.totalCount, 'star')}`, 265 | `${getAge(repo.createdAt)}`, 266 | `${formatPlural(repo.commits.length, 'commit')}`, 267 | `${(repo.diskUsage / 1000).toFixed(2)}MB`, 268 | ]; 269 | const url = `https://github.com/${user.login}/${repo.name}`; 270 | return ` 271 |
272 |
273 |

274 | ${repo.name} 275 |

276 | ${props.join(', ')}
277 | Languages: ${repo.languages.nodes 278 | .map(l => l.name) 279 | .join(',')} 280 |
281 |
282 |
    283 | ${ 284 | repo.descriptionHTML 285 | ? `
  • ${repo.descriptionHTML}
  • ` 286 | : '' 287 | } 288 | ${ 289 | repo.homepageUrl 290 | ? `
  • ${repo.homepageUrl}
  • ` 291 | : '' 292 | } 293 |
294 |
295 |
296 | `; 297 | }) 298 | .join('')} 299 |
300 |
301 | `; 302 | if (repos.length > 1) { 303 | timeline(normalizeData(repos), repos, $('#timeline')); 304 | $('#timeline-mode').addEventListener('change', () => { 305 | const mode = $('#timeline-mode').value; 306 | timeline(normalizeData(repos, mode), repos, $('#timeline')); 307 | }); 308 | } else { 309 | $('#timeline-mode').style.display = 'none'; 310 | } 311 | piechart(languages, $('#piechart')); 312 | } 313 | 314 | export default function UI() { 315 | return { 316 | renderLoading, 317 | renderTokenRequiredForm, 318 | renderProfileRequiredForm, 319 | renderLoader, 320 | renderReport, 321 | }; 322 | } 323 | -------------------------------------------------------------------------------- /site/public/app.js: -------------------------------------------------------------------------------- 1 | !function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="/",e(e.s=15)}([function(t,n,e){t.exports=e(16)},function(t,n){function e(t,n,e,r,o,a,i){try{var c=t[a](i),s=c.value}catch(t){return void e(t)}c.done?n(s):Promise.resolve(s).then(r,o)}t.exports=function(t){return function(){var n=this,r=arguments;return new Promise(function(o,a){var i=t.apply(n,r);function c(t){e(i,o,a,c,s,"next",t)}function s(t){e(i,o,a,c,s,"throw",t)}c(void 0)})}}},function(t,n,e){var r=e(12)(Object,"create");t.exports=r},function(t,n,e){var r=e(50);t.exports=function(t,n){for(var e=t.length;e--;)if(r(t[e][0],n))return e;return-1}},function(t,n,e){var r=e(56);t.exports=function(t,n){var e=t.__data__;return r(n)?e["string"==typeof n?"string":"hash"]:e.map}},function(t,n,e){var r=e(23);t.exports=function(t,n,e){var o=null==t?void 0:r(t,n);return void 0===o?e:o}},function(t,n){var e=Array.isArray;t.exports=e},function(t,n,e){var r=e(11),o=e(29),a="[object Symbol]";t.exports=function(t){return"symbol"==typeof t||o(t)&&r(t)==a}},function(t,n,e){var r=e(9).Symbol;t.exports=r},function(t,n,e){var r=e(26),o="object"==typeof self&&self&&self.Object===Object&&self,a=r||o||Function("return this")();t.exports=a},function(t,n){var e;e=function(){return this}();try{e=e||new Function("return this")()}catch(t){"object"==typeof window&&(e=window)}t.exports=e},function(t,n,e){var r=e(8),o=e(27),a=e(28),i="[object Null]",c="[object Undefined]",s=r?r.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?c:i:s&&s in Object(t)?o(t):a(t)}},function(t,n,e){var r=e(37),o=e(42);t.exports=function(t,n){var e=o(t,n);return r(e)?e:void 0}},function(t,n){t.exports=function(t){var n=typeof t;return null!=t&&("object"==n||"function"==n)}},function(t,n,e){"use strict";var r=e(17),o=e(19);function a(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}n.parse=b,n.resolve=function(t,n){return b(t,!1,!0).resolve(n)},n.resolveObject=function(t,n){return t?b(t,!1,!0).resolveObject(n):n},n.format=function(t){o.isString(t)&&(t=b(t));return t instanceof a?t.format():a.prototype.format.call(t)},n.Url=a;var i=/^([a-z0-9.+-]+:)/i,c=/:[0-9]*$/,s=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,u=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(u),h=["%","/","?",";","#"].concat(l),f=["/","?","#"],p=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,v={javascript:!0,"javascript:":!0},m={javascript:!0,"javascript:":!0},g={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},y=e(20);function b(t,n,e){if(t&&o.isObject(t)&&t instanceof a)return t;var r=new a;return r.parse(t,n,e),r}a.prototype.parse=function(t,n,e){if(!o.isString(t))throw new TypeError("Parameter 'url' must be a string, not "+typeof t);var a=t.indexOf("?"),c=-1!==a&&a127?R+="x":R+=P[q];if(!R.match(p)){var I=S.slice(0,L),U=S.slice(L+1),z=P.match(d);z&&(I.push(z[1]),U.unshift(z[2])),U.length&&(b="/"+U.join(".")+b),this.hostname=I.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),E||(this.hostname=r.toASCII(this.hostname));var N=this.port?":"+this.port:"",M=this.hostname||"";this.host=M+N,this.href+=this.host,E&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==b[0]&&(b="/"+b))}if(!v[O])for(L=0,A=l.length;L0)&&e.host.split("@"))&&(e.auth=E.shift(),e.host=e.hostname=E.shift());return e.search=t.search,e.query=t.query,o.isNull(e.pathname)&&o.isNull(e.search)||(e.path=(e.pathname?e.pathname:"")+(e.search?e.search:"")),e.href=e.format(),e}if(!_.length)return e.pathname=null,e.search?e.path="/"+e.search:e.path=null,e.href=e.format(),e;for(var k=_.slice(-1)[0],C=(e.host||t.host||_.length>1)&&("."===k||".."===k)||""===k,L=0,T=_.length;T>=0;T--)"."===(k=_[T])?_.splice(T,1):".."===k?(_.splice(T,1),L++):L&&(_.splice(T,1),L--);if(!w&&!O)for(;L--;L)_.unshift("..");!w||""===_[0]||_[0]&&"/"===_[0].charAt(0)||_.unshift(""),C&&"/"!==_.join("/").substr(-1)&&_.push("");var E,S=""===_[0]||_[0]&&"/"===_[0].charAt(0);j&&(e.hostname=e.host=S?"":_.length?_.shift():"",(E=!!(e.host&&e.host.indexOf("@")>0)&&e.host.split("@"))&&(e.auth=E.shift(),e.host=e.hostname=E.shift()));return(w=w||e.host&&_.length)&&!S&&_.unshift(""),_.length?e.pathname=_.join("/"):(e.pathname=null,e.path=null),o.isNull(e.pathname)&&o.isNull(e.search)||(e.path=(e.pathname?e.pathname:"")+(e.search?e.search:"")),e.auth=t.auth||e.auth,e.slashes=e.slashes||t.slashes,e.href=e.format(),e},a.prototype.parseHost=function(){var t=this.host,n=c.exec(t);n&&(":"!==(n=n[0])&&(this.port=n.substr(1)),t=t.substr(0,t.length-n.length)),t&&(this.hostname=t)}},function(t,n,e){t.exports=e(64)},function(t,n,e){var r=function(t){"use strict";var n,e=Object.prototype,r=e.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",i=o.asyncIterator||"@@asyncIterator",c=o.toStringTag||"@@toStringTag";function s(t,n,e,r){var o=n&&n.prototype instanceof v?n:v,a=Object.create(o.prototype),i=new L(r||[]);return a._invoke=function(t,n,e){var r=l;return function(o,a){if(r===f)throw new Error("Generator is already running");if(r===p){if("throw"===o)throw a;return E()}for(e.method=o,e.arg=a;;){var i=e.delegate;if(i){var c=j(i,e);if(c){if(c===d)continue;return c}}if("next"===e.method)e.sent=e._sent=e.arg;else if("throw"===e.method){if(r===l)throw r=p,e.arg;e.dispatchException(e.arg)}else"return"===e.method&&e.abrupt("return",e.arg);r=f;var s=u(t,n,e);if("normal"===s.type){if(r=e.done?p:h,s.arg===d)continue;return{value:s.arg,done:e.done}}"throw"===s.type&&(r=p,e.method="throw",e.arg=s.arg)}}}(t,e,i),a}function u(t,n,e){try{return{type:"normal",arg:t.call(n,e)}}catch(t){return{type:"throw",arg:t}}}t.wrap=s;var l="suspendedStart",h="suspendedYield",f="executing",p="completed",d={};function v(){}function m(){}function g(){}var y={};y[a]=function(){return this};var b=Object.getPrototypeOf,x=b&&b(b(T([])));x&&x!==e&&r.call(x,a)&&(y=x);var w=g.prototype=v.prototype=Object.create(y);function O(t){["next","throw","return"].forEach(function(n){t[n]=function(t){return this._invoke(n,t)}})}function _(t,n){var e;this._invoke=function(o,a){function i(){return new n(function(e,i){!function e(o,a,i,c){var s=u(t[o],t,a);if("throw"!==s.type){var l=s.arg,h=l.value;return h&&"object"==typeof h&&r.call(h,"__await")?n.resolve(h.__await).then(function(t){e("next",t,i,c)},function(t){e("throw",t,i,c)}):n.resolve(h).then(function(t){l.value=t,i(l)},function(t){return e("throw",t,i,c)})}c(s.arg)}(o,a,e,i)})}return e=e?e.then(i,i):i()}}function j(t,e){var r=t.iterator[e.method];if(r===n){if(e.delegate=null,"throw"===e.method){if(t.iterator.return&&(e.method="return",e.arg=n,j(t,e),"throw"===e.method))return d;e.method="throw",e.arg=new TypeError("The iterator does not provide a 'throw' method")}return d}var o=u(r,t.iterator,e.arg);if("throw"===o.type)return e.method="throw",e.arg=o.arg,e.delegate=null,d;var a=o.arg;return a?a.done?(e[t.resultName]=a.value,e.next=t.nextLoc,"return"!==e.method&&(e.method="next",e.arg=n),e.delegate=null,d):a:(e.method="throw",e.arg=new TypeError("iterator result is not an object"),e.delegate=null,d)}function k(t){var n={tryLoc:t[0]};1 in t&&(n.catchLoc=t[1]),2 in t&&(n.finallyLoc=t[2],n.afterLoc=t[3]),this.tryEntries.push(n)}function C(t){var n=t.completion||{};n.type="normal",delete n.arg,t.completion=n}function L(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(k,this),this.reset(!0)}function T(t){if(t){var e=t[a];if(e)return e.call(t);if("function"==typeof t.next)return t;if(!isNaN(t.length)){var o=-1,i=function e(){for(;++o=0;--a){var i=this.tryEntries[a],c=i.completion;if("root"===i.tryLoc)return o("end");if(i.tryLoc<=this.prev){var s=r.call(i,"catchLoc"),u=r.call(i,"finallyLoc");if(s&&u){if(this.prev=0;--e){var o=this.tryEntries[e];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--n){var e=this.tryEntries[n];if(e.finallyLoc===t)return this.complete(e.completion,e.afterLoc),C(e),d}},catch:function(t){for(var n=this.tryEntries.length-1;n>=0;--n){var e=this.tryEntries[n];if(e.tryLoc===t){var r=e.completion;if("throw"===r.type){var o=r.arg;C(e)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:T(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=n),d}},t}(t.exports);try{regeneratorRuntime=r}catch(t){Function("r","regeneratorRuntime = r")(r)}},function(t,n,e){(function(t,r){var o;/*! https://mths.be/punycode v1.4.1 by @mathias */!function(a){n&&n.nodeType,t&&t.nodeType;var i="object"==typeof r&&r;i.global!==i&&i.window!==i&&i.self;var c,s=2147483647,u=36,l=1,h=26,f=38,p=700,d=72,v=128,m="-",g=/^xn--/,y=/[^\x20-\x7E]/,b=/[\x2E\u3002\uFF0E\uFF61]/g,x={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},w=u-l,O=Math.floor,_=String.fromCharCode;function j(t){throw new RangeError(x[t])}function k(t,n){for(var e=t.length,r=[];e--;)r[e]=n(t[e]);return r}function C(t,n){var e=t.split("@"),r="";return e.length>1&&(r=e[0]+"@",t=e[1]),r+k((t=t.replace(b,".")).split("."),n).join(".")}function L(t){for(var n,e,r=[],o=0,a=t.length;o=55296&&n<=56319&&o65535&&(n+=_((t-=65536)>>>10&1023|55296),t=56320|1023&t),n+=_(t)}).join("")}function E(t,n){return t+22+75*(t<26)-((0!=n)<<5)}function S(t,n,e){var r=0;for(t=e?O(t/p):t>>1,t+=O(t/n);t>w*h>>1;r+=u)t=O(t/w);return O(r+(w+1)*t/(t+f))}function A(t){var n,e,r,o,a,i,c,f,p,g,y,b=[],x=t.length,w=0,_=v,k=d;for((e=t.lastIndexOf(m))<0&&(e=0),r=0;r=128&&j("not-basic"),b.push(t.charCodeAt(r));for(o=e>0?e+1:0;o=x&&j("invalid-input"),((f=(y=t.charCodeAt(o++))-48<10?y-22:y-65<26?y-65:y-97<26?y-97:u)>=u||f>O((s-w)/i))&&j("overflow"),w+=f*i,!(f<(p=c<=k?l:c>=k+h?h:c-k));c+=u)i>O(s/(g=u-p))&&j("overflow"),i*=g;k=S(w-a,n=b.length+1,0==a),O(w/n)>s-_&&j("overflow"),_+=O(w/n),w%=n,b.splice(w++,0,_)}return T(b)}function P(t){var n,e,r,o,a,i,c,f,p,g,y,b,x,w,k,C=[];for(b=(t=L(t)).length,n=v,e=0,a=d,i=0;i=n&&yO((s-e)/(x=r+1))&&j("overflow"),e+=(c-n)*x,n=c,i=0;is&&j("overflow"),y==n){for(f=e,p=u;!(f<(g=p<=a?l:p>=a+h?h:p-a));p+=u)k=f-g,w=u-g,C.push(_(E(g+k%w,0))),f=O(k/w);C.push(_(E(f,0))),a=S(e,x,r==o),e=0,++r}++e,++n}return C.join("")}c={version:"1.4.1",ucs2:{decode:L,encode:T},decode:A,encode:P,toASCII:function(t){return C(t,function(t){return y.test(t)?"xn--"+P(t):t})},toUnicode:function(t){return C(t,function(t){return g.test(t)?A(t.slice(4).toLowerCase()):t})}},void 0===(o=function(){return c}.call(n,e,n,t))||(t.exports=o)}()}).call(this,e(18)(t),e(10))},function(t,n){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),t.webpackPolyfill=1),t}},function(t,n,e){"use strict";t.exports={isString:function(t){return"string"==typeof t},isObject:function(t){return"object"==typeof t&&null!==t},isNull:function(t){return null===t},isNullOrUndefined:function(t){return null==t}}},function(t,n,e){"use strict";n.decode=n.parse=e(21),n.encode=n.stringify=e(22)},function(t,n,e){"use strict";function r(t,n){return Object.prototype.hasOwnProperty.call(t,n)}t.exports=function(t,n,e,a){n=n||"&",e=e||"=";var i={};if("string"!=typeof t||0===t.length)return i;var c=/\+/g;t=t.split(n);var s=1e3;a&&"number"==typeof a.maxKeys&&(s=a.maxKeys);var u=t.length;s>0&&u>s&&(u=s);for(var l=0;l=0?(h=v.substr(0,m),f=v.substr(m+1)):(h=v,f=""),p=decodeURIComponent(h),d=decodeURIComponent(f),r(i,p)?o(i[p])?i[p].push(d):i[p]=[i[p],d]:i[p]=d}return i};var o=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)}},function(t,n,e){"use strict";var r=function(t){switch(typeof t){case"string":return t;case"boolean":return t?"true":"false";case"number":return isFinite(t)?t:"";default:return""}};t.exports=function(t,n,e,c){return n=n||"&",e=e||"=",null===t&&(t=void 0),"object"==typeof t?a(i(t),function(i){var c=encodeURIComponent(r(i))+e;return o(t[i])?a(t[i],function(t){return c+encodeURIComponent(r(t))}).join(n):c+encodeURIComponent(r(t[i]))}).join(n):c?encodeURIComponent(r(c))+e+encodeURIComponent(r(t)):""};var o=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function a(t,n){if(t.map)return t.map(n);for(var e=[],r=0;r-1}},function(t,n,e){var r=e(3);t.exports=function(t,n){var e=this.__data__,o=r(e,t);return o<0?(++this.size,e.push([t,n])):e[o][1]=n,this}},function(t,n,e){var r=e(12)(e(9),"Map");t.exports=r},function(t,n,e){var r=e(4);t.exports=function(t){var n=r(this,t).delete(t);return this.size-=n?1:0,n}},function(t,n){t.exports=function(t){var n=typeof t;return"string"==n||"number"==n||"symbol"==n||"boolean"==n?"__proto__"!==t:null===t}},function(t,n,e){var r=e(4);t.exports=function(t){return r(this,t).get(t)}},function(t,n,e){var r=e(4);t.exports=function(t){return r(this,t).has(t)}},function(t,n,e){var r=e(4);t.exports=function(t,n){var e=r(this,t),o=e.size;return e.set(t,n),this.size+=e.size==o?0:1,this}},function(t,n,e){var r=e(61);t.exports=function(t){return null==t?"":r(t)}},function(t,n,e){var r=e(8),o=e(62),a=e(6),i=e(7),c=1/0,s=r?r.prototype:void 0,u=s?s.toString:void 0;t.exports=function t(n){if("string"==typeof n)return n;if(a(n))return o(n,t)+"";if(i(n))return u?u.call(n):"";var e=n+"";return"0"==e&&1/n==-c?"-0":e}},function(t,n){t.exports=function(t,n){for(var e=-1,r=null==t?0:t.length,o=Array(r);++e1&&void 0!==i[1]?i[1]:{},t.prev=1,t.next=4,fetch("https://api.github.com/graphql",{headers:Object.assign({},{"Content-Type":"application/json",Authorization:"token ".concat(l)},e),method:"POST",body:JSON.stringify({query:n})});case 4:if((r=t.sent).ok){t.next=7;break}throw new Error("".concat(r.status," ").concat(r.statusText));case 7:return t.next=9,r.json();case 9:return(a=t.sent).errors&&(console.warn("There are errors while requesting ".concat("https://api.github.com/graphql")),console.warn(a.errors.map(function(t){return t.message}))),t.abrupt("return",a);case 14:t.prev=14,t.t0=t.catch(1),t.t0.toString().match(/401 Unauthorized/)?(localStorage.removeItem("OCTOLIFE_GH_TOKEN"),window.location.href="/"):document.querySelector("#root").innerHTML='\n
\n

Error

\n

Please try again in a few minutes.

\n
\n ';case 17:case"end":return t.stop()}},t,null,[[1,14]])}));return function(n){return t.apply(this,arguments)}}(),f=function(t,n){return'\n query {\n search(query: "user:'.concat(t,'", type: REPOSITORY, first: 100').concat(n?', after: "'.concat(n,'"'):"",") {\n repositoryCount,\n edges {\n cursor,\n node {\n ... on Repository {\n name,\n createdAt,\n descriptionHTML,\n diskUsage,\n forkCount,\n homepageUrl,\n stargazers {\n totalCount\n },\n issues(states: OPEN) {\n totalCount\n },\n languages(first:15) {\n nodes {\n name,\n color\n }\n }\n }\n }\n }\n }\n }\n")},p=function(t,n,e){return'\n {\n repository(owner: "'.concat(t,'", name: "').concat(n,'") {\n object(expression: "master") {\n ... on Commit {\n history(first: 100').concat(e?', after: "'.concat(e,'"'):"",") {\n nodes {\n committedDate\n }\n pageInfo {\n endCursor\n }\n }\n }\n }\n }\n }\n")},d=function(t){return'\n query {\n search(query: "user:'.concat(t,'", type: USER, first: 1) {\n userCount,\n edges {\n node {\n ... on User {\n name,\n login,\n avatarUrl,\n bio,\n company,\n createdAt,\n location,\n url,\n websiteUrl,\n followers {\n totalCount\n }\n }\n }\n }\n }\n }\n')};function v(t,n){var e=Math.abs(n-t);return Math.ceil(e/864e5)}function m(t,n){return 1===t?"".concat(t," ").concat(n):"".concat(t," ").concat(n,"s")}function g(t){return m(Math.ceil(v(new Date,new Date(t))/365),"year")}function y(t,n,e){e.innerHTML='\n

'.concat(m(t.length,"repo"),", ").concat(m(t.reduce(function(t,n){return t+n.totalNumOfCommits},0),"commit"),"

"),TimelinesChart()(e).zScaleLabel("units").width(window.innerWidth-100).leftMargin(200).rightMargin(10).zQualitative(!0).maxLineHeight(20).timeFormat("%Y-%m-%d").maxHeight(24*n.length).zColorScale(d3.scaleOrdinal(n.map(function(t){return t.name}),n.map(function(t){return"#000"}))).data(t),setTimeout(function(){document.querySelector(".legend").setAttribute("style","display: none")},10)}function b(t){var n=new Date(t);return"".concat(n.getFullYear(),"-").concat(n.getMonth()+1,"-").concat(n.getDate())}function x(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"all",e=null,r=null;return n.match(/^year/)?e=Number(n.replace("year","")):n.match(/^language_/)&&(r=n.replace("language_","")),t.map(function(t){if(0===t.commits.length)return!1;if(r&&!t.languages.nodes.find(function(t){return t.name===r}))return!1;var n=[],o=t.commits.reduce(function(t,n){var e=b(n);return t[e]||(t[e]=0),t[e]+=1,t},{}),a=Object.keys(o).sort(function(t,n){return new Date(t).getTime()-new Date(n).getTime()}).map(function(t){return new Date(t)});if(e&&(a=a.filter(function(t){return t.getFullYear()===e})),0===a.length)return!1;var i=a.shift(),c=i,s=Object.keys(o).reduce(function(t,n){return t+o[n]},0);return a.forEach(function(e){v(e,i)>0&&(n.push({timeRange:[b(c),b(i)],val:t.name}),c=e),i=e}),n.push({timeRange:[b(c),b(i)],val:t.name}),{totalNumOfCommits:s,group:t.name,data:[{label:"",data:n}]}}).filter(function(t){return t}).sort(function(t,n){return n.totalNumOfCommits-t.totalNumOfCommits})}var w=function(t){return document.querySelector(t)},O=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];function _(t){w("#root").innerHTML='\n
\n '.concat('\n

Octlife

\n

Your (public) life on GitHub

\n ','\n
\n

\n ').concat(t,"\n

\n
\n ")}function j(t){w("#root").innerHTML='\n
\n '.concat('\n

Octlife

\n

Your (public) life on GitHub

\n ','\n
\n

\n Authorize Octolife GitHub app
to see the report
\n

\n
\n

📌 Octolife caches your profile for 90 days so other users can see it without authorization.

\n
\n ')}function k(t,n){w("#root").innerHTML='\n
\n '.concat('\n

Octlife

\n

Your (public) life on GitHub

\n ',"\n
\n ").concat(n?'

'.concat(n,"

"):"",'\n

\n \n Enter a GitHub profile name and hit Enter.\n

\n
\n

🤔 Wonder how an Octolife report looks like? Go check one here.

\n
\n ');var e=w("#github-profile");e.addEventListener("keyup",function(n){if(13===n.keyCode){var r=e.value;""===r?e.style["outline-color"]="red":t(r)}}),setTimeout(function(){e.focus()},20)}function C(){w("#root").innerHTML='\n
\n '.concat('\n

Octlife

\n

Your (public) life on GitHub

\n ','\n
\n

\n
\n ');var t=w("#loader-content"),n=[];return function(e){arguments.length>1&&void 0!==arguments[1]&&arguments[1]?n[n.length-1]=e:n.push(e),t.innerHTML=n.map(function(t){return"
".concat(t,"
")}).join("")}}function L(t,n){history.pushState({},"Octolife / ".concat(t.name),"/".concat(t.login));var e=function(t){return t.reduce(function(t,n){return n.languages.nodes.forEach(function(n){var e=t.find(function(t){return t.name===n.name});e?e.value+=1:t.push({name:n.name,color:n.color,value:1})}),t},[])}(n).sort(function(t,n){return n.value-t.value}),r=e.reduce(function(t,n){return t+n.value},0),o=n.reduce(function(t,n){return n.commits.forEach(function(n){var e=new Date(n).getFullYear();t.includes(e)||t.push(e)}),t},[]).sort(function(t,n){return t-n}),a=n.reduce(function(t,n){return n.commits.forEach(function(n){var e=O[new Date(n).getDay()];void 0===t[e]&&(t[e]=0),t[e]+=1}),t},{}),i=O.reduce(function(t,n){return t+(a[n]||0)},0),c=n.reduce(function(t,n){return n.commits.forEach(function(n){var e=new Date(n).getHours();void 0===t[e]&&(t[e]=0),t[e]+=1}),t},{}),s=Object.keys(c).reduce(function(t,n){return t+(c[n]||0)},0);w("#root").innerHTML='\n
\n
\n

\n Octgithublife\n

\n New Report\n
\n
\n

').concat(t.name,'').concat(t.name,'

\n

🌟 ').concat(m(function(t){return t.reduce(function(t,n){return t+n.stargazers.totalCount},0)}(n),"star"),'

\n
\n
\n
\n
    \n
  • @GitHub: ').concat(t.url,"
  • \n ").concat(t.websiteUrl?'
  • @Web: ').concat(t.websiteUrl,"
  • "):"","\n
  • Age: ").concat(g(t.createdAt),"
  • \n ").concat(t.location?"
  • Location: ".concat(t.location,"
  • "):"","\n ").concat(t.company?"
  • Location: ".concat(t.company,"
  • "):"","\n
  • Repositories: ").concat(n.length,"
  • \n
  • Followers: ").concat(t.followers.totalCount,"
  • \n
\n
\n
\n ").concat(t.bio?"

".concat(t.bio,"

"):"","\n ").concat(t.pinnedRepositories&&t.pinnedRepositories.nodes.length>0?"

Pins: ".concat(t.pinnedRepositories.nodes.map(function(t){return'').concat(t.name,"(★").concat(t.stargazers.totalCount,")")}).join(", "),"

"):"",'\n
\n
\n
\n
\n
\n
\n
\n
\n
').concat(e.map(function(t){var n=(t.value/r*100).toFixed(1);return'
\n  \n ').concat(n,"% ").concat(t.name,"\n
")}).join(""),'
\n
\n
\n
\n

Time

\n
\n
\n
').concat(O.map(function(t){var n=(a[t]/i*100).toFixed(1);return'
\n  \n ').concat(n,"%   ").concat(t,"\n
")}).join(""),"
\n
\n ").concat(Object.keys(c).map(function(t){var n=(c[t]/s*100).toFixed(1);return'
\n  \n ').concat(n,"%   ").concat(function(t){return t<10?"0".concat(t,":00"):"".concat(t,":00")}(t),"\n
")}).join(""),'\n
\n
\n
\n
\n

Timeline (commit history)

\n
\n \n
\n
\n
\n

Repositories

\n
\n ').concat(n.sort(function(t,n){return n.stargazers.totalCount-t.stargazers.totalCount}).map(function(n){var e=["".concat(m(n.stargazers.totalCount,"star")),"".concat(g(n.createdAt)),"".concat(m(n.commits.length,"commit")),"".concat((n.diskUsage/1e3).toFixed(2),"MB")],r="https://github.com/".concat(t.login,"/").concat(n.name);return'\n
\n
\n

\n ').concat(n.name,"\n

\n ").concat(e.join(", "),"
\n Languages: ").concat(n.languages.nodes.map(function(t){return t.name}).join(","),"\n
\n
\n
    \n ").concat(n.descriptionHTML?"
  • ".concat(n.descriptionHTML,"
  • "):"","\n ").concat(n.homepageUrl?'
  • ').concat(n.homepageUrl,"
  • "):"","\n
\n
\n
\n ")}).join(""),"\n
\n
\n "),n.length>1?(y(x(n),n,w("#timeline")),w("#timeline-mode").addEventListener("change",function(){var t=w("#timeline-mode").value;y(x(n,t),n,w("#timeline"))})):w("#timeline-mode").style.display="none",function(t,n){var e=Math.min(300,300)/2,r=(d3.scaleOrdinal(d3.schemeCategory10),d3.pie().value(function(t){return t.value})),o=d3.arc().innerRadius(e-100).outerRadius(e-20),a=d3.select(n).append("svg").attr("width",300).attr("height",300).append("g").attr("transform","translate(".concat(150,",").concat(150,")")).datum(t).selectAll("path").data(r).enter().append("path").attr("fill",function(t,n){return t.data.color}).attr("d",o).each(function(t){this._current=t.value});d3.selectAll("input").on("change",c);var i=setTimeout(function(){d3.select('input[value="oranges"]').property("checked",!0).each(c)},2e3);function c(){var t=this.value;clearTimeout(i),r.value(function(n){return n[t]}),(a=a.data(r)).transition().duration(750).attrTween("d",s)}function s(t){var n=d3.interpolate(this._current,t);return this._current=n(0),function(t){return o(n(t))}}}(e,w("#piechart"))}var T="s-maxage=".concat(7776e3);function E(t){return S.apply(this,arguments)}function S(){return(S=i()(o.a.mark(function t(n){var e,r,a;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return r=[],a=function(){var t=i()(o.a.mark(function t(){var i,c,s;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return i=f(n,e),t.next=3,h(i);case 3:if(c=t.sent,s=c.data,r=r.concat(s.search.edges),!(s.search.repositoryCount>r.length)){t.next=9;break}return e=r[r.length-1].cursor.replace("==",""),t.abrupt("return",a());case 9:return t.abrupt("return",r.map(function(t){return t.node}));case 10:case"end":return t.stop()}},t)}));return function(){return t.apply(this,arguments)}}(),t.abrupt("return",a());case 3:case"end":return t.stop()}},t)}))).apply(this,arguments)}function A(t,n){return P.apply(this,arguments)}function P(){return(P=i()(o.a.mark(function t(n,e){var r,a,c;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return 100,a=[],c=function(){var t=i()(o.a.mark(function t(){var i,s,l,f;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return i=p(n,e,r),t.next=3,h(i);case 3:if(s=t.sent,l=s.data,a=a.concat(u()(l,"repository.object.history.nodes")),!(f=u()(l,"repository.object.history.pageInfo.endCursor"))){t.next=10;break}return r=f,t.abrupt("return",c());case 10:return t.abrupt("return",a.map(function(t){return t&&t.committedDate||!1}).filter(function(t){return t}));case 11:case"end":return t.stop()}},t)}));return function(){return t.apply(this,arguments)}}(),t.abrupt("return",c());case 4:case"end":return t.stop()}},t)}))).apply(this,arguments)}function R(t){return q.apply(this,arguments)}function q(){return(q=i()(o.a.mark(function t(n){var e,r;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,h(d(n));case 2:return e=t.sent,r=e.data,t.abrupt("return",u()(r,"search.edges.0.node",null));case 5:case"end":return t.stop()}},t)}))).apply(this,arguments)}function F(t,n,e){return I.apply(this,arguments)}function I(){return(I=i()(o.a.mark(function t(n,e,r){var a,c,s;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return s=function(){return(s=i()(o.a.mark(function t(){var i,s;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:if(!(a>=e.length)){t.next=2;break}return t.abrupt("return");case 2:return i=e[a],s=Math.ceil(a/e.length*100),r("⌛ Getting commit history (".concat(s,"%)"),!0),t.next=7,A(n,i.name);case 7:return i.commits=t.sent,a+=1,t.next=11,c();case 11:case"end":return t.stop()}},t)}))).apply(this,arguments)},c=function(){return s.apply(this,arguments)},a=0,t.next=5,c();case 5:case"end":return t.stop()}},t)}))).apply(this,arguments)}function U(t,n){return z.apply(this,arguments)}function z(){return(z=i()(o.a.mark(function t(n,e){var r;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,fetch("/octolife-api/cache?user=".concat(n.login),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({user:n,repos:e})});case 3:return r=t.sent,t.t0=console,t.t1="Cache: ",t.t2=JSON,t.next=9,r.json();case 9:t.t3=t.sent,t.t4=t.t2.stringify.call(t.t2,t.t3),t.t5=t.t1.concat.call(t.t1,t.t4),t.t0.log.call(t.t0,t.t5),N(n.login),t.next=19;break;case 16:t.prev=16,t.t6=t.catch(0),console.log(t.t6);case 19:case"end":return t.stop()}},t,null,[[0,16]])}))).apply(this,arguments)}function N(t){return M.apply(this,arguments)}function M(){return(M=i()(o.a.mark(function t(n){var e,r;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,fetch("/octolife-api/cache?user=".concat(n),{method:"GET",headers:{"Content-Type":"application/json","Cache-Control":T}});case 3:return e=t.sent,t.next=6,e.json();case 6:if(!(r=t.sent).data){t.next=9;break}return t.abrupt("return",r.data);case 9:return t.abrupt("return",!1);case 12:return t.prev=12,t.t0=t.catch(0),t.abrupt("return",!1);case 15:case"end":return t.stop()}},t,null,[[0,12]])}))).apply(this,arguments)}window.addEventListener("load",i()(o.a.mark(function t(){var n,e,r,a,s,u,h,f,p,d,v,m;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:if(m=function(){return(m=i()(o.a.mark(function t(n){var e,i,c;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:if(f){t.next=3;break}return u(n),t.abrupt("return");case 3:return(e=r())("⌛ Getting profile information ..."),t.next=7,R(n);case 7:if(null!==(i=t.sent)){t.next=12;break}a(v,'⚠️ There is no user with profile name "'.concat(n,'". Try again.')),t.next=24;break;case 12:return e("✅ Profile information.",!0),e("⌛ Getting ".concat(i.name,"'s repositories ...")),t.next=16,E(i.login);case 16:return c=t.sent,e("✅ ".concat(i.name,"'s repositories."),!0),e("⌛ Getting commit history ..."),t.next=21,F(i.login,c,e);case 21:e("✅ Commits.",!0),U(i,c),s(i,c);case 24:case"end":return t.stop()}},t)}))).apply(this,arguments)},v=function(t){return m.apply(this,arguments)},d=function(){return(d=i()(o.a.mark(function t(n){var r;return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return e("⌛ Loading. Please wait."),t.next=3,N(n);case 3:if(!((r=t.sent)&&r.user&&r.repos)){t.next=7;break}return s(r.user,r.repos),t.abrupt("return",!0);case 7:return t.abrupt("return",!1);case 8:case"end":return t.stop()}},t)}))).apply(this,arguments)},p=function(t){return d.apply(this,arguments)},e=(n={renderLoading:_,renderTokenRequiredForm:j,renderProfileRequiredForm:k,renderLoader:C,renderReport:L}).renderLoading,r=n.renderLoader,a=n.renderProfileRequiredForm,s=n.renderReport,u=n.renderTokenRequiredForm,(h=Object(c.parse)(window.location.href).path.replace(/^\//,"").split("/").shift()).match(/^\?/)&&(h=""),f=localStorage.getItem("OCTOLIFE_GH_TOKEN"),l=f,""===h){t.next=17;break}return t.next=12,p(h);case 12:if(!t.sent){t.next=14;break}return t.abrupt("return");case 14:v(h),t.next=18;break;case 17:a(function(){var t=i()(o.a.mark(function t(n){return o.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,p(n);case 2:if(!t.sent){t.next=4;break}return t.abrupt("return");case 4:v(n);case 5:case"end":return t.stop()}},t)}));return function(n){return t.apply(this,arguments)}}());case 18:case"end":return t.stop()}},t)})))}]); --------------------------------------------------------------------------------