├── .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 | 
--------------------------------------------------------------------------------
/.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 |
Oct
life
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 |
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 | Oct
life
21 | Your (public) life on GitHub
22 | `;
23 | }
24 |
25 | function renderLoading(message) {
26 | $('#root').innerHTML = `
27 |
34 | `;
35 | }
36 |
37 | function renderTokenRequiredForm(profileNameFromTheURL) {
38 | $('#root').innerHTML = `
39 |
48 | `;
49 | }
50 |
51 | function renderProfileRequiredForm(profileNameProvided, message) {
52 | $('#root').innerHTML = `
53 |
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 |
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 |
148 |
149 |
${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 |
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 |
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 ';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 ")}function j(t){w("#root").innerHTML='\n \n ')}function k(t,n){w("#root").innerHTML='\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 ');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
').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 \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(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)})))}]);
--------------------------------------------------------------------------------