From d6e28e17bfc6a56a3d89ca5b24a81d132b94027c Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Thu, 14 Jan 2021 18:29:27 +0100 Subject: [PATCH] Use faker.js for mocked data and placeholder (#47) --- .github/pull_request_template.md | 69 +- package-lock.json | 6 + package.json | 3 +- settings.example.json | 1 + source/app/metrics.mjs | 132 +--- source/app/mocks.mjs | 622 +++++++++--------- source/app/setup.mjs | 7 +- source/app/web/instance.mjs | 121 ++-- source/app/web/statics/app.js | 157 +++-- source/app/web/statics/app.placeholder.js | 530 +++++++++++++++ source/app/web/statics/index.html | 349 +++------- source/app/web/statics/style.css | 440 ++++++------- source/plugins/activity/index.mjs | 2 +- source/plugins/people/index.mjs | 2 +- source/templates/classic/image.svg | 2 +- source/templates/classic/partials/people.ejs | 6 +- source/templates/repository/image.svg | 2 +- source/templates/terminal/image.svg | 2 +- .../terminal/partials/base.header.ejs | 2 +- 19 files changed, 1385 insertions(+), 1070 deletions(-) create mode 100644 source/app/web/statics/app.placeholder.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6fdf9bb1..212714ef 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,35 +1,40 @@ - -**Pull request description** - - -**Additional context and screenshots** - diff --git a/package-lock.json b/package-lock.json index caaece7b..d132c086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2690,6 +2690,12 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "faker": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-5.1.0.tgz", + "integrity": "sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index ebf23fcb..759f6caf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node source/app/web/index.mjs", "test": "npx jest", - "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest vue-prism-component@latest jest@latest js-yaml@latest libxmljs@latest" + "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest vue-prism-component@latest faker@latest jest@latest js-yaml@latest libxmljs@latest" }, "repository": { "type": "git", @@ -38,6 +38,7 @@ "vue-prism-component": "^1.2.0" }, "devDependencies": { + "faker": "^5.1.0", "jest": "^26.6.3", "js-yaml": "^4.0.0", "libxmljs": "^0.19.7" diff --git a/settings.example.json b/settings.example.json index 3db0a2fb..c3ce40b2 100644 --- a/settings.example.json +++ b/settings.example.json @@ -10,6 +10,7 @@ "port":3000, "//":"Listening port", "optimize":true, "//":"Optimize SVG image", "debug":false, "//":"Debug mode", + "mocked":false, "//":"Use mocked data", "repositories":100, "//":"Number of repositories to use to compute metrics", "templates":{ "//":"Template configuration", "default":"classic", "//":"Default template", diff --git a/source/app/metrics.mjs b/source/app/metrics.mjs index 42f26cad..6476823c 100644 --- a/source/app/metrics.mjs +++ b/source/app/metrics.mjs @@ -44,49 +44,43 @@ console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`) } - //Placeholder - if (login === "placeholder") - placeholder({data, conf, q}) - //Compute - else { - //Query data from GitHub API - console.debug(`metrics/compute/${login} > graphql query`) - const forks = q["repositories.forks"] || false - Object.assign(data, await graphql(queries.common({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks:forks ? "" : ", isFork: false"}))) - //Query repositories from GitHub API - { - //Iterate through repositories - let cursor = null - let pushed = 0 - do { - console.debug(`metrics/compute/${login} > retrieving repositories after ${cursor}`) - const {user:{repositories:{edges, nodes}}} = await graphql(queries.repositories({login, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, 100), forks:forks ? "" : ", isFork: false"})) - cursor = edges?.[edges?.length-1]?.cursor - data.user.repositories.nodes.push(...nodes) - pushed = nodes.length - } while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories)) - //Limit repositories - console.debug(`metrics/compute/${login} > keeping only ${repositories} repositories`) - data.user.repositories.nodes.splice(repositories) - console.debug(`metrics/compute/${login} > loaded ${data.user.repositories.nodes.length} repositories`) - } - //Compute metrics - console.debug(`metrics/compute/${login} > compute`) - const computer = Templates[template].default || Templates[template] - await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}}) - const promised = await Promise.all(pending) + //Query data from GitHub API + console.debug(`metrics/compute/${login} > graphql query`) + const forks = q["repositories.forks"] || false + Object.assign(data, await graphql(queries.common({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks:forks ? "" : ", isFork: false"}))) + //Query repositories from GitHub API + { + //Iterate through repositories + let cursor = null + let pushed = 0 + do { + console.debug(`metrics/compute/${login} > retrieving repositories after ${cursor}`) + const {user:{repositories:{edges, nodes}}} = await graphql(queries.repositories({login, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, 100), forks:forks ? "" : ", isFork: false"})) + cursor = edges?.[edges?.length-1]?.cursor + data.user.repositories.nodes.push(...nodes) + pushed = nodes.length + } while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories)) + //Limit repositories + console.debug(`metrics/compute/${login} > keeping only ${repositories} repositories`) + data.user.repositories.nodes.splice(repositories) + console.debug(`metrics/compute/${login} > loaded ${data.user.repositories.nodes.length} repositories`) + } + //Compute metrics + console.debug(`metrics/compute/${login} > compute`) + const computer = Templates[template].default || Templates[template] + await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}}) + const promised = await Promise.all(pending) - //Check plugins errors - { - const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors] - if (errors.length) { - console.warn(`metrics/compute/${login} > ${errors.length} errors !`) - if (die) - throw new Error(`An error occured during rendering, dying`) - else - console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256})) - } - } + //Check plugins errors + { + const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors] + if (errors.length) { + console.warn(`metrics/compute/${login} > ${errors.length} errors !`) + if (die) + throw new Error(`An error occured during rendering, dying`) + else + console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256})) + } } //Template rendering @@ -230,57 +224,3 @@ await page.close() return {resized, mime} } - -/** Placeholder generator */ - function placeholder({data, conf, q}) { - //Proxifier - const proxify = (target) => (typeof target === "object")&&(target) ? new Proxy(target, { - get(target, property) { - //Primitive conversion - if (property === Symbol.toPrimitive) - return () => "##" - //Iterables - if (property === Symbol.iterator) - return Reflect.get(target, property) - //Plugins should not be proxified by default as they can be toggled by user - if (/^plugins$/.test(property)) - return Reflect.get(target, property) - //Consider no errors on plugins - if (/^error/.test(property)) - return undefined - //Proxify recursively - return proxify(property in target ? Reflect.get(target, property) : {}) - } - }) : target - //Enabled plugins - const enabled = Object.entries(conf.settings.plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).filter(key => (key in q)&&(q[key])) - //Placeholder data - Object.assign(data, { - s(_, letter) { return letter === "y" ? "ies" : "s" }, - meta:{version:conf.package.version, author:conf.package.author, placeholder:true}, - user:proxify({name:`############`, websiteUrl:`########################`, isHireable:false}), - computed:proxify({ - avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - registration:"## years ago", - cakeday:false, - calendar:new Array(14).fill({color:"#ebedf0"}), - licenses:{favorite:`########`}, - token:{scopes:[]}, - }), - plugins:Object.fromEntries(enabled.map(key => - [key, proxify({ - posts:{source:"########", list:new Array(2).fill({title:"###### ###### ####### ######", date:"####"})}, - music:{provider:"########", tracks:new Array(3).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})}, - pagespeed:{detailed:!!q["pagespeed.detailed"], screenshot:!!q["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null, scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))}, - followup:{issues:{count:0}, pr:{count:0}}, - habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}}, - languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))}, - topics:{mode:"topics.mode" in q ? q["topics.mode"] : "starred", list:[...new Array(12).fill(null).map(() => ({name:"######", description:"", icon:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})), {name:`And ## more...`, description:"", icon:null}]}, - projects:{list:[...new Array(2).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]}, - tweets:{profile:{username:"########", verified:false}, list:[...new Array(2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]}, - stars:{repositories:[...new Array(2).fill({node:{nameWithOwner:"########/########", description:"###### ###### ####### ######".repeat(4)}})]}, - activity:{events:[{type:"comment", on:"pr"}, {type:"public"}, {type:"release"}, {type:"issue"}]} - }[key]??{})] - )), - }) - } diff --git a/source/app/mocks.mjs b/source/app/mocks.mjs index fe6c7408..89663543 100644 --- a/source/app/mocks.mjs +++ b/source/app/mocks.mjs @@ -1,6 +1,7 @@ //Imports import axios from "axios" import urls from "url" + import faker from "faker" //Mocked state let mocked = false @@ -22,78 +23,80 @@ apply(target, that, args) { //Arguments const [query] = args + const login = query.match(/login: "(?.*?)"/)?.groups?.login ?? faker.internet.userName() + //Common query if (/^query Metrics /.test(query)) { console.debug(`metrics/compute/mocks > mocking graphql api result > Metrics`) return ({ user: { - databaseId:22963968, - name:"Simon Lecoq", - login:"lowlighter", - createdAt:"2016-10-20T16:49:29Z", - avatarUrl:"https://avatars0.githubusercontent.com/u/22963968?u=f5097de6f06ed2e31906f784163fc1e9fc84ed57&v=4", - websiteUrl:"https://simon.lecoq.io", - isHireable:false, - twitterUsername:"lecoqsimon", - repositories:{totalCount:Math.floor(Math.random()*100), totalDiskUsage:Math.floor(Math.random()*100000), nodes:[]}, - packages:{totalCount:Math.floor(Math.random()*10)}, - starredRepositories:{totalCount:Math.floor(Math.random()*1000)}, - watching:{totalCount:Math.floor(Math.random()*100)}, - sponsorshipsAsSponsor:{totalCount:Math.floor(Math.random()*5)}, - sponsorshipsAsMaintainer:{totalCount:Math.floor(Math.random()*5)}, + databaseId:faker.random.number(10000000), + name:faker.name.findName(), + login, + createdAt:`${faker.date.past(10)}`, + avatarUrl:faker.image.people(), + websiteUrl:faker.internet.url(), + isHireable:faker.random.boolean(), + twitterUsername:login, + repositories:{totalCount:faker.random.number(100), totalDiskUsage:faker.random.number(100000), nodes:[]}, + packages:{totalCount:faker.random.number(10)}, + starredRepositories:{totalCount:faker.random.number(1000)}, + watching:{totalCount:faker.random.number(100)}, + sponsorshipsAsSponsor:{totalCount:faker.random.number(10)}, + sponsorshipsAsMaintainer:{totalCount:faker.random.number(10)}, contributionsCollection:{ - totalRepositoriesWithContributedCommits:Math.floor(Math.random()*30), - totalCommitContributions:Math.floor(Math.random()*1000), - restrictedContributionsCount:Math.floor(Math.random()*500), - totalIssueContributions:Math.floor(Math.random()*100), - totalPullRequestContributions:Math.floor(Math.random()*100), - totalPullRequestReviewContributions:Math.floor(Math.random()*100) + totalRepositoriesWithContributedCommits:faker.random.number(100), + totalCommitContributions:faker.random.number(10000), + restrictedContributionsCount:faker.random.number(10000), + totalIssueContributions:faker.random.number(100), + totalPullRequestContributions:faker.random.number(1000), + totalPullRequestReviewContributions:faker.random.number(1000), }, calendar:{ contributionCalendar:{ weeks:[ { contributionDays:[ - {color:"#40c463"}, - {color:"#ebedf0"}, - {color:"#9be9a8"}, - {color:"#ebedf0"}, - {color:"#ebedf0"} + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, ] }, { contributionDays:[ - {color:"#30a14e"}, - {color:"#9be9a8"}, - {color:"#40c463"}, - {color:"#9be9a8"}, - {color:"#ebedf0"}, - {color:"#ebedf0"}, - {color:"#ebedf0"} + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, ] }, { contributionDays:[ - {color:"#40c463"}, - {color:"#216e39"}, - {color:"#9be9a8"} + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, + {color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])}, ] } ] } }, - repositoriesContributedTo:{totalCount:Math.floor(Math.random()*10)}, - followers:{totalCount:Math.floor(Math.random()*100)}, - following:{totalCount:Math.floor(Math.random()*100)}, - issueComments:{totalCount:Math.floor(Math.random()*100)}, - organizations:{totalCount:Math.floor(Math.random()*5)} + repositoriesContributedTo:{totalCount:faker.random.number(100)}, + followers:{totalCount:faker.random.number(1000)}, + following:{totalCount:faker.random.number(1000)}, + issueComments:{totalCount:faker.random.number(1000)}, + organizations:{totalCount:faker.random.number(10)} } }) } //Repositories query if (/^query Repositories /.test(query)) { console.debug(`metrics/compute/mocks > mocking graphql api result > Repositories`) - return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({ + return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ repositories:{ edges:[], @@ -105,29 +108,32 @@ repositories:{ edges:[ { - cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + cursor:"MOCKED_CURSOR" }, ], nodes:[ { - name:"metrics", - watchers:{totalCount:Math.floor(Math.random()*100)}, - stargazers:{totalCount:Math.floor(Math.random()*1000)}, - owner:{login:"lowlighter"}, + name:faker.random.words(), + watchers:{totalCount:faker.random.number(1000)}, + stargazers:{totalCount:faker.random.number(10000)}, + owner:{login}, languages:{ edges:[ - {size:111733, node:{color:"#f1e05a", name:"JavaScript"} - }, - {size:14398, node:{color:"#563d7c", name:"CSS"}}, - {size:13223, node:{color:"#e34c26", name:"HTML"}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, ] }, - issues_open:{totalCount:Math.floor(Math.random()*100)}, - issues_closed:{totalCount:Math.floor(Math.random()*100)}, - pr_open:{totalCount:Math.floor(Math.random()*100)}, - pr_merged:{totalCount:Math.floor(Math.random()*100)}, - releases:{totalCount:Math.floor(Math.random()*100)}, - forkCount:Math.floor(Math.random()*100), + issues_open:{totalCount:faker.random.number(100)}, + issues_closed:{totalCount:faker.random.number(100)}, + pr_open:{totalCount:faker.random.number(100)}, + pr_merged:{totalCount:faker.random.number(100)}, + releases:{totalCount:faker.random.number(100)}, + forkCount:faker.random.number(100), licenseInfo:{spdxId:"MIT"} }, ] @@ -142,25 +148,29 @@ user:{ repository:{ name:"metrics", - owner:{login:"lowlighter"}, + owner:{login}, createdAt:new Date().toISOString(), diskUsage:Math.floor(Math.random()*10000), - watchers:{totalCount:Math.floor(Math.random()*100)}, - stargazers:{totalCount:Math.floor(Math.random()*1000)}, + homepageUrl:faker.internet.url(), + watchers:{totalCount:faker.random.number(1000)}, + stargazers:{totalCount:faker.random.number(10000)}, languages:{ edges:[ - {size:111733, node:{color:"#f1e05a", name:"JavaScript"} - }, - {size:14398, node:{color:"#563d7c", name:"CSS"}}, - {size:13223, node:{color:"#e34c26", name:"HTML"}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, + {size:faker.random.number(100000), node:{color:faker.internet.color(), name:faker.lorem.word()}}, ] }, - issues_open:{totalCount:Math.floor(Math.random()*100)}, - issues_closed:{totalCount:Math.floor(Math.random()*100)}, - pr_open:{totalCount:Math.floor(Math.random()*100)}, - pr_merged:{totalCount:Math.floor(Math.random()*100)}, - releases:{totalCount:Math.floor(Math.random()*100)}, - forkCount:Math.floor(Math.random()*100), + issues_open:{totalCount:faker.random.number(100)}, + issues_closed:{totalCount:faker.random.number(100)}, + pr_open:{totalCount:faker.random.number(100)}, + pr_merged:{totalCount:faker.random.number(100)}, + releases:{totalCount:faker.random.number(100)}, + forkCount:faker.random.number(100), licenseInfo:{spdxId:"MIT"} }, } @@ -181,7 +191,7 @@ contributionDays = [] } //Random contributions - const contributionCount = Math.min(10, Math.max(0, Math.floor(Math.random()*14-4))) + const contributionCount = Math.min(10, Math.max(0, faker.random.number(14)-4)) contributionDays.push({ contributionCount, color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)], @@ -204,14 +214,21 @@ return ({ user:{ gists:{ - totalCount:1, + totalCount:faker.random.number(100), nodes:[ { - stargazerCount:Math.floor(Math.random()*10), + stargazerCount:faker.random.number(10), isFork:false, - forks:{totalCount:Math.floor(Math.random()*10)}, - files:[{name:"example"}], - comments:{totalCount:Math.floor(Math.random()*10)} + forks:{totalCount:faker.random.number(10)}, + files:[{name:faker.system.fileName()}], + comments:{totalCount:faker.random.number(10)} + }, + { + stargazerCount:faker.random.number(10), + isFork:false, + forks:{totalCount:faker.random.number(10)}, + files:[{name:faker.system.fileName()}], + comments:{totalCount:faker.random.number(10)} } ] } @@ -228,11 +245,11 @@ nodes:[ { name:"User-owned project", - updatedAt:new Date().toISOString(), + updatedAt:`${faker.date.recent()}`, progress:{ - doneCount:Math.floor(Math.random()*10), - inProgressCount:Math.floor(Math.random()*10), - todoCount:Math.floor(Math.random()*10), + doneCount:faker.random.number(10), + inProgressCount:faker.random.number(10), + todoCount:faker.random.number(10), enabled:true } } @@ -249,11 +266,11 @@ repository:{ project:{ name:"Repository project example", - updatedAt:new Date().toISOString(), + updatedAt:`${faker.date.recent()}`, progress:{ - doneCount:Math.floor(Math.random()*10), - inProgressCount:Math.floor(Math.random()*10), - todoCount:Math.floor(Math.random()*10), + doneCount:faker.random.number(10), + inProgressCount:faker.random.number(10), + todoCount:faker.random.number(10), enabled:true } } @@ -269,20 +286,20 @@ starredRepositories:{ edges:[ { - starredAt:"2020-10-16T18:53:16Z", + starredAt:`${faker.date.recent(14)}`, node:{ description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !", - forkCount:12, + forkCount:faker.random.number(100), isFork:false, issues:{ - totalCount: 12 + totalCount:faker.random.number(100), }, nameWithOwner:"lowlighter/metrics", openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a", pullRequests:{ - totalCount:23 + totalCount:faker.random.number(100), }, - stargazerCount:120, + stargazerCount:faker.random.number(10000), licenseInfo:{ nickname:null, name:"MIT License" @@ -301,7 +318,7 @@ //Stargazers query if (/^query Stargazers /.test(query)) { console.debug(`metrics/compute/mocks > mocking graphql api result > Stargazers`) - return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({ + return /after: "MOCKED_CURSOR"/m.test(query) ? ({ repository:{ stargazers:{ edges:[], @@ -310,9 +327,9 @@ }) : ({ repository:{ stargazers:{ - edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map(() => ({ - starredAt:new Date(Date.now()-Math.floor(30*Math.random())*24*60*60*1000).toISOString(), - cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + edges:new Array(faker.random.number({min:50, max:100})).fill(null).map(() => ({ + starredAt:`${faker.date.recent(30)}`, + cursor:"MOCKED_CURSOR" })) } } @@ -322,7 +339,7 @@ if (/^query People /.test(query)) { console.debug(`metrics/compute/mocks > mocking graphql api result > People`) const type = query.match(/(?followers|following)[(]/)?.groups?.type ?? "(unknown type)" - return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({ + return /after: "MOCKED_CURSOR"/m.test(query) ? ({ user:{ [type]:{ edges:[], @@ -331,11 +348,11 @@ }) : ({ user:{ [type]:{ - edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map(() => ({ - cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + edges:new Array(Math.ceil(20+80*Math.random())).fill(undefined).map((login = faker.internet.userName()) => ({ + cursor:"MOCKED_CURSOR", node:{ - login:"user", - avatarUrl:"https://github.com/identicons/user.png", + login, + avatarUrl:null, } })) } @@ -369,7 +386,7 @@ if (/^HEAD .$/.test(url)) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.request HEAD`) return ({ - status: 200, + status:200, url:"https://api.github.com/", headers:{ server:"GitHub.com", @@ -380,37 +397,37 @@ }) } //Commit content - if (/^https:..api.github.com.repos.lowlighter.metrics.commits.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.test(url)) { + if (/^https:..api.github.com.repos.lowlighter.metrics.commits.MOCKED_SHA/.test(url)) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`) return ({ - status: 200, - url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + status:200, + url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA", data:{ - sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + sha:"MOCKED_SHA", commit:{ author:{ - name:"lowlighter", - email:"22963968+lowlighter@users.noreply.github.com", - date:new Date().toISOString(), + name:faker.internet.userName(), + email:faker.internet.email(), + date:`${faker.date.recent(7)}`, }, committer:{ - name:"lowlighter", - email:"22963968+lowlighter@users.noreply.github.com", - date:new Date().toISOString(), + name:faker.internet.userName(), + email:faker.internet.email(), + date:`${faker.date.recent(7)}`, }, }, author:{ - login:"lowlighter", - id:22963968, + login:faker.internet.userName(), + id:faker.random.number(100000000), }, committer:{ - login:"lowlighter", - id:22963968, + login:faker.internet.userName(), + id:faker.random.number(100000000), }, files: [ { - sha:"5ab8c4fb6a0be4c157419c3b9d7b522dca354b3f", - filename:"index.mjs", + sha:"MOCKED_SHA", + filename:faker.system.fileName(), patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file" }, ] @@ -426,7 +443,7 @@ rest.rateLimit.get = new Proxy(unmocked.rateLimit, { apply:function(target, that, args) { return ({ - status: 200, + status:200, url:"https://api.github.com/rate_limit", headers:{ server:"GitHub.com", @@ -435,12 +452,12 @@ }, data:{ resources:{ - core:{limit:5000, used:0, remaining:5000, reset:0 }, - search:{limit:30, used:0, remaining:30, reset:0 }, - graphql:{limit:5000, used:0, remaining:5000, reset:0 }, - integration_manifest:{limit:5000, used:0, remaining:5000, reset:0 }, - source_import:{limit:100, used:0, remaining:100, reset:0 }, - code_scanning_upload:{limit:500, used:0, remaining:500, reset:0 }, + core:{limit:5000, used:0, remaining:5000, reset:0}, + search:{limit:30, used:0, remaining:30, reset:0}, + graphql:{limit:5000, used:0, remaining:5000, reset:0}, + integration_manifest:{limit:5000, used:0, remaining:5000, reset:0}, + source_import:{limit:100, used:0, remaining:100, reset:0}, + code_scanning_upload:{limit:500, used:0, remaining:500, reset:0}, }, rate:{limit:5000, used:0, remaining:"MOCKED", reset:0} } @@ -450,11 +467,11 @@ //Events list rest.activity.listEventsForAuthenticatedUser = new Proxy(unmocked.listEventsForAuthenticatedUser, { - apply:function(target, that, [{page, per_page}]) { + apply:function(target, that, [{username:login, page, per_page}]) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser`) return ({ status:200, - url:`https://api.github.com/users/lowlighter/events?per_page=${per_page}&page=${page}`, + url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`, headers:{ server:"GitHub.com", status:"200 OK", @@ -465,160 +482,159 @@ id:"10000000000", type:"CommitCommentEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ comment:{ user:{ - login:"lowlighter", + login, }, - path:"README.md", - commit_id:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - body:"This is a commit comment", + path:faker.system.fileName(), + commit_id:"MOCKED_SHA", + body:faker.lorem.sentence(), } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000001", type:"PullRequestReviewCommentEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ action:"created", comment:{ user:{ - login:"lowlighter", + login, }, - body:"This is pull request review comment", + body:faker.lorem.paragraph(), }, pull_request:{ - title:"Pull request example", + title:faker.lorem.sentence(), number:1, user:{ - login:"lowlighter", + login:faker.internet.userName(), }, body:"", } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000002", type:"IssuesEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ - action:"closed", + action:faker.random.arrayElement(["opened", "closed", "reopened"]), issue:{ number:2, - title:"Issue example", + title:faker.lorem.sentence(), user:{ - login:"lowlighter", + login, }, - body:"Hello this is an example", - performed_via_github_app: null + body:faker.lorem.paragraph(), + performed_via_github_app:null } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000003", type:"GollumEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/lowlighter", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ pages:[ { - page_name:"Home", - title:"Home", + page_name:faker.lorem.sentence(), + title:faker.lorem.sentence(), summary:null, action:"created", - sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + sha:"MOCKED_SHA", } ] }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000004", type:"IssueCommentEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ action:"created", issue:{ number:3, - title:"Issue example", + title:faker.lorem.sentence(), user:{ - login:"lowlighter", + login, }, labels:[ { - name:"question", + name:"lorem ipsum", color:"d876e3", } ], state:"open", }, comment:{ - body:"Hello world !", - performed_via_github_app: null + body:faker.lorem.paragraph(), + performed_via_github_app:null } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000005", type:"ForkEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - id:327522930, - name:"lowlighter/gracidea", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ forkee:{ - name:"gracidea", - full_name:"lowlighter/gracidea", + name:faker.random.word(), + full_name:`${faker.random.word()}/${faker.random.word()}`, } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000006", type:"PullRequestReviewEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ action:"created", review:{ user:{ - login:"lowlighter", + login, }, state:"approved", }, @@ -626,151 +642,151 @@ state:"open", number:4, locked:false, - title:"Pull request example", + title:faker.lorem.sentence(), user:{ - login:"user", + login:faker.internet.userName(), }, } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000007", type:"ReleaseEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ action:"published", release:{ - tag_name:"v3.1", - name:"Version 3.1", - draft: false, - prerelease: true, + tag_name:`v${faker.random.number()}.${faker.random.number()}`, + name:faker.random.words(4), + draft:faker.random.boolean(), + prerelease:faker.random.boolean(), } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000008", type:"CreateEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ - ref:"feat-new-plugin", - ref_type:"branch", + ref:faker.lorem.slug(), + ref_type:faker.random.arrayElement(["tag", "branch"]), master_branch:"master", }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"100000000009", type:"WatchEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/gracidea", + name:"lowlighter/metrics", }, payload:{action:"started"}, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000010", type:"DeleteEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ - ref:"feat-plugin-merged", - ref_type:"branch", + ref:faker.lorem.slug(), + ref_type:faker.random.arrayElement(["tag", "branch"]), }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000011", type:"PushEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ size:1, ref:"refs/heads/master", commits:[ { - sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - message:"Commit example", + sha:"MOCKED_SHA", + message:faker.lorem.sentence(), } ] }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000012", type:"PullRequestEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ - action:"opened", + action:faker.random.arrayElement(["opened", "closed"]), number:5, pull_request:{ user:{ - login:"lowlighter", + login, }, state:"open", - title:"Pull request example", - additions:210, - deletions:126, - changed_files:10, + title:faker.lorem.sentence(), + additions:faker.random.number(1000), + deletions:faker.random.number(1000), + changed_files:faker.random.number(10), } }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000013", type:"MemberEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{ member:{ - login:"botlighter", + login:faker.internet.userName(), }, action:"added" }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), }, { id:"10000000014", type:"PublicEvent", actor:{ - login:"lowlighter", + login, }, repo:{ - name:"lowlighter/metrics", + name:`${faker.random.word()}/${faker.random.word()}`, }, payload:{}, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + created_at:faker.date.recent(7), } ] }) @@ -779,13 +795,13 @@ //Repository traffic rest.repos.getViews = new Proxy(unmocked.getViews, { - apply:function(target, that, args) { + apply:function(target, that, [{owner, repo}]) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getViews`) - const count = Math.floor(Math.random()*1000)*2 - const uniques = Math.floor(Math.random()*count)*2 + const count = faker.random.number(10000)*2 + const uniques = faker.random.number(count)*2 return ({ status:200, - url:"https://api.github.com/repos/lowlighter/metrics/traffic/views", + url:`https://api.github.com/repos/${owner}/${repo}/traffic/views`, headers:{ server:"GitHub.com", status:"200 OK", @@ -795,8 +811,8 @@ count, uniques, views:[ - {timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2}, - {timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2}, + {timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2}, + {timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2}, ] } }) @@ -805,27 +821,27 @@ //Repository contributions rest.repos.getContributorsStats = new Proxy(unmocked.getContributorsStats, { - apply:function(target, that, args) { + apply:function(target, that, [{owner, repo}]) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats`) return ({ status:200, - url:"https://api.github.com/repos/lowlighter/metrics/stats/contributors", - headers: { + url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`, + headers:{ server:"GitHub.com", status:"200 OK", "x-oauth-scopes":"repo", }, data:[ { - total:Math.floor(Math.random()*1000), + total:faker.random.number(10000), weeks:[ - {w:1, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)}, - {w:2, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)}, - {w:3, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)}, - {w:4, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)}, + {w:1, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, + {w:2, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, + {w:3, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, + {w:4, a:faker.random.number(10000), d:faker.random.number(10000), c:faker.random.number(10000)}, ], author: { - login:"lowlighter", + login:owner, } } ] @@ -835,11 +851,11 @@ //Repository contributions rest.repos.listCommits = new Proxy(unmocked.listCommits, { - apply:function(target, that, [{page, per_page}]) { + apply:function(target, that, [{page, per_page, owner, repo}]) { console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listCommits`) return ({ status:200, - url:`https://api.github.com/repos/lowlighter/metrics/commits?per_page=${per_page}&page=${page}`, + url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`, headers: { server:"GitHub.com", status:"200 OK", @@ -847,15 +863,15 @@ }, data:page < 2 ? new Array(per_page).fill(null).map(() => ({ - sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + sha:"MOCKED_SHA", commit:{ author:{ - name:"lowlighter", - date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString() + name:owner, + date:`${faker.date.recent(14)}` }, committer:{ - name:"lowlighter", - date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString() + name:owner, + date:`${faker.date.recent(14)}` }, } }) @@ -904,6 +920,7 @@ apply:function(target, that, args) { //Arguments const [url, options] = args + const tested = url.match(/&url=(?.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url() //Pagespeed api if (/^https:..www.googleapis.com.pagespeedonline.v5/.test(url)) { //Pagespeed result @@ -913,10 +930,10 @@ status:200, data:{ captchaResult:"CAPTCHA_NOT_NEEDED", - id:"https://simon.lecoq.io/", + id:tested, lighthouseResult:{ - requestedUrl:"https://simon.lecoq.io/", - finalUrl:"https://simon.lecoq.io/", + requestedUrl:tested, + finalUrl:tested, lighthouseVersion:"6.3.0", audits:{ "final-screenshot":{ @@ -936,41 +953,41 @@ details:{ items:[ { - observedFirstContentfulPaint:283, - observedFirstVisualChangeTs:1789259909429, - observedFirstContentfulPaintTs:1789259857628, - firstContentfulPaint:370, - observedDomContentLoaded:251, - observedFirstMeaningfulPaint:642, - maxPotentialFID:203, - observedLoad:330, - firstMeaningfulPaint:370, - observedCumulativeLayoutShift:0.0028944855967078186, - observedSpeedIndex:711, - observedSpeedIndexTs:1789260285891, - observedTimeOriginTs:1789259574429, - observedLargestContentfulPaint:857, - cumulativeLayoutShift:0.0028944855967078186, - observedFirstPaintTs:1789259857628, - observedTraceEndTs:1789261300953, - largestContentfulPaint:1085, - observedTimeOrigin:0, - speedIndex:578, - observedTraceEnd:1727, - observedDomContentLoadedTs:1789259825567, - observedFirstPaint:283, - totalBlockingTime:133, - observedLastVisualChangeTs:1789260426429, - observedFirstVisualChange:335, - observedLargestContentfulPaintTs:1789260431554, - estimatedInputLatency:13, - observedLoadTs:1789259904916, - observedLastVisualChange:852, - firstCPUIdle:773, - interactive:953, - observedNavigationStartTs:1789259574429, - observedNavigationStart:0, - observedFirstMeaningfulPaintTs:1789260216895 + observedFirstContentfulPaint:faker.random.number(500), + observedFirstVisualChangeTs:faker.time.recent(), + observedFirstContentfulPaintTs:faker.time.recent(), + firstContentfulPaint:faker.random.number(500), + observedDomContentLoaded:faker.random.number(500), + observedFirstMeaningfulPaint:faker.random.number(1000), + maxPotentialFID:faker.random.number(500), + observedLoad:faker.random.number(500), + firstMeaningfulPaint:faker.random.number(500), + observedCumulativeLayoutShift:faker.random.float({max:1}), + observedSpeedIndex:faker.random.number(1000), + observedSpeedIndexTs:faker.time.recent(), + observedTimeOriginTs:faker.time.recent(), + observedLargestContentfulPaint:faker.random.number(1000), + cumulativeLayoutShift:faker.random.float({max:1}), + observedFirstPaintTs:faker.time.recent(), + observedTraceEndTs:faker.time.recent(), + largestContentfulPaint:faker.random.number(2000), + observedTimeOrigin:faker.random.number(10), + speedIndex:faker.random.number(1000), + observedTraceEnd:faker.random.number(2000), + observedDomContentLoadedTs:faker.time.recent(), + observedFirstPaint:faker.random.number(500), + totalBlockingTime:faker.random.number(500), + observedLastVisualChangeTs:faker.time.recent(), + observedFirstVisualChange:faker.random.number(500), + observedLargestContentfulPaintTs:faker.time.recent(), + estimatedInputLatency:faker.random.number(100), + observedLoadTs:faker.time.recent(), + observedLastVisualChange:faker.random.number(1000), + firstCPUIdle:faker.random.number(1000), + interactive:faker.random.number(1000), + observedNavigationStartTs:faker.time.recent(), + observedNavigationStart:faker.random.number(10), + observedFirstMeaningfulPaintTs:faker.time.recent() }, ] }, @@ -980,26 +997,26 @@ "best-practices":{ id:"best-practices", title:"Best Practices", - score:Math.floor(Math.random()*100)/100, + score:faker.random.float({max:1}), }, seo:{ id:"seo", title:"SEO", - score:Math.floor(Math.random()*100)/100, + score:faker.random.float({max:1}), }, accessibility:{ id:"accessibility", title:"Accessibility", - score:Math.floor(Math.random()*100)/100, + score:faker.random.float({max:1}), }, performance: { id:"performance", title:"Performance", - score:Math.floor(Math.random()*100)/100, + score:faker.random.float({max:1}), } }, }, - analysisUTCTimestamp:new Date().toISOString() + analysisUTCTimestamp:`${faker.date.recent()}`, } }) } @@ -1009,6 +1026,8 @@ //Get recently played tracks if (/me.player.recently-played/.test(url)&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) { console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`) + const artist = faker.random.words() + const track = faker.random.words(5) return ({ status:200, data:{ @@ -1019,42 +1038,42 @@ album_type:"single", artists:[ { - name:"EGOIST", + name:artist, type:"artist", } ], images:[ { height:640, - url:"https://i.scdn.co/image/ab67616d0000b27366371d0ad05c3f402d9cb2ae", + url:faker.image.abstract(), width:640 }, { height:300, - url:"https://i.scdn.co/image/ab67616d00001e0266371d0ad05c3f402d9cb2ae", + url:faker.image.abstract(), width:300 }, { height:64, - url:"https://i.scdn.co/image/ab67616d0000485166371d0ad05c3f402d9cb2ae", + url:faker.image.abstract(), width:64 } ], - name:"Fallen", - release_date:"2014-11-19", + name:track, + release_date:`${faker.date.past()}`.substring(0, 10), type:"album", }, artists:[ { - name:"EGOIST", + name:artist, type:"artist", } ], - name:"Fallen", - preview_url:"https://p.scdn.co/mp3-preview/f30eb6d1c55afa13ce754559a41ab683a1a76b02?cid=fa6ae353840041ee8af3bd1d21a66783", + name:track, + preview_url:faker.internet.url(), type:"track", }, - played_at:new Date().toISOString(), + played_at:`${faker.date.recent()}`, context:{ type:"album", } @@ -1069,15 +1088,16 @@ //Get user profile if ((/users.by.username/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) { console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`) + const username = url.match(/username[/](?.*?)[?]/)?.groups?.username ?? faker.internet.userName() return ({ status:200, data:{ data:{ - profile_image_url:"https://pbs.twimg.com/profile_images/1338344493234286592/C_ujKIUa_normal.png", - name:"GitHub", - verified:true, - id:"13334762", - username:"github", + profile_image_url:faker.image.people(), + name:faker.name.findName(), + verified:faker.random.boolean(), + id:faker.random.number(1000000).toString(), + username, }, } }) @@ -1090,8 +1110,8 @@ data:{ data:[ { - id:"1000000000000000001", - created_at:new Date().toISOString(), + id:faker.random.number(100000000000000).toString(), + created_at:`${faker.date.recent()}`, entities:{ mentions:[ {start:22, end:33, username:"lowlighter"}, @@ -1100,25 +1120,25 @@ text:"Checkout metrics from @lowlighter ! #GitHub", }, { - id:"1000000000000000000", - created_at:new Date().toISOString(), - text:"Hello world !", + id:faker.random.number(100000000000000).toString(), + created_at:`${faker.date.recent()}`, + text:faker.lorem.paragraph(), } ], includes:{ users:[ { - id:"100000000000000000", + id:faker.random.number(100000000000000).toString(), name:"lowlighter", username:"lowlighter", }, ] }, meta:{ - newest_id:"1000000000000000001", - oldest_id:"1000000000000000000", + newest_id:faker.random.number(100000000000000).toString(), + oldest_id:faker.random.number(100000000000000).toString(), result_count:2, - next_token:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + next_token:"MOCKED_CURSOR", }, } }) diff --git a/source/app/setup.mjs b/source/app/setup.mjs index 855a3b1f..74a43552 100644 --- a/source/app/setup.mjs +++ b/source/app/setup.mjs @@ -26,8 +26,11 @@ templates:{}, queries:{}, settings:{}, - statics:__statics, - node_modules:__modules, + paths:{ + statics:__statics, + templates:__templates, + node_modules:__modules, + } } //Load settings diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index 6e1209c0..554d6769 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -11,12 +11,12 @@ import metrics from "../metrics.mjs" /** App */ - export default async function ({mock = false, nosettings = false} = {}) { + export default async function ({mock, nosettings} = {}) { //Load configuration settings const {conf, Plugins, Templates} = await setup({nosettings}) const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings - cache.placeholder = new cache.Cache() + mock = mock || conf.settings.mocked //Apply configuration mocking if needed if (mock) { @@ -60,8 +60,7 @@ } //Cache headers middleware middlewares.push((req, res, next) => { - if (!["/placeholder"].includes(req.path)) - res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache") + res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache") next() }) @@ -70,61 +69,62 @@ const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false})) const enabled = Object.entries(Plugins).map(([name]) => ({name, enabled:plugins[name]?.enabled ?? false})) const actions = {flush:new Map()} - app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`)) - app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`)) - app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.statics}/favicon.png`)) - app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.statics}/favicon.png`)) - app.get("/.opengraph.png", limiter, (req, res) => res.sendFile(`${conf.statics}/opengraph.png`)) - app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version)) - app.get("/.requests", limiter, async (req, res) => res.status(200).json((await rest.rateLimit.get()).data.rate)) - app.get("/.templates", limiter, (req, res) => res.status(200).json(templates)) - app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled)) - app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts)) - app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.statics}/style.css`)) - app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.statics}/style.vars.css`)) - app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/themes/prism-tomorrow.css`)) - app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.statics}/app.js`)) - app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/ejs/ejs.min.js`)) - app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.js`)) - app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.map`)) - app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue/dist/vue.min.js`)) - app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`)) - app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`)) - app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/prism.js`)) - app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/components/prism-yaml.min.js`)) - app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/components/prism-markdown.min.js`)) - app.get("/.uncache", limiter, async (req, res) => { - const {token, user} = req.query - if (token) { - if (actions.flush.has(token)) { - console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`) - cache.del(actions.flush.get(token)) - return res.sendStatus(200) + let requests = (await rest.rateLimit.get()).data.rate + setInterval(async () => requests = (await rest.rateLimit.get()).data.rate, 30*1000) + //Web + app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`)) + app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`)) + app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`)) + app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`)) + app.get("/.opengraph.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/opengraph.png`)) + //Plugins and templates + app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled)) + app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts)) + app.get("/.templates", limiter, (req, res) => res.status(200).json(templates)) + app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404)) + for (const template in conf.templates) + app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`)) + //Styles + app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`)) + app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`)) + app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`)) + //Scripts + app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`)) + app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`)) + app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`)) + app.get("/.js/faker.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/faker/dist/faker.min.js`)) + app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.js`)) + app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.map`)) + app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue/dist/vue.min.js`)) + app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`)) + app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`)) + app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`)) + app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`)) + app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`)) + //Meta + app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version)) + app.get("/.requests", limiter, async (req, res) => res.status(200).json(requests)) + //Cache + app.get("/.uncache", limiter, async (req, res) => { + const {token, user} = req.query + if (token) { + if (actions.flush.has(token)) { + console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`) + cache.del(actions.flush.get(token)) + return res.sendStatus(200) + } + else + return res.sendStatus(404) } - else - return res.sendStatus(404) - } - else { - const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}` - actions.flush.set(token, user) - return res.json({token}) - } - }) + else { + const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}` + actions.flush.set(token, user) + return res.json({token}) + } + }) //Metrics app.get("/:login", ...middlewares, async (req, res) => { - //Placeholder hash - const placeholder = Object.entries(parse(req.query)).filter(([key, value]) => - ((key in Plugins)&&(!!value))|| - ((key === "template")&&(value in Templates))|| - (/base[.](header|activity|community|repositories|metadata)/.test(key))|| - (["pagespeed.detailed", "pagespeed.screenshot", "habits.charts", "habits.facts", "topics.mode"].includes(key)) - ).map(([key, value]) => `${key}${ - key === "template" ? `#${value}` : - key === "topics.mode" ? `#${value === "mastered" ? value : "starred"}` : - !!value - }`).sort().join("+") - //Request params const {login} = req.params if ((restricted.length)&&(!restricted.includes(login))) { @@ -132,13 +132,6 @@ return res.sendStatus(403) } //Read cached data if possible - //Placeholder - if ((login === "placeholder")&&(cache.placeholder.get(placeholder))) { - const {rendered, mime} = cache.placeholder.get(placeholder) - res.header("Content-Type", mime) - res.send(rendered) - return - } //User cached if ((!debug)&&(cached)&&(cache.get(login))) { const {rendered, mime} = cache.get(login) @@ -164,8 +157,6 @@ convert:["jpeg", "png"].includes(q["config.output"]) ? q["config.output"] : null }, {Plugins, Templates}) //Cache - if (login === "placeholder") - cache.placeholder.put(placeholder, {rendered, mime}) if ((!debug)&&(cached)) cache.put(login, {rendered, mime}, cached) //Send response @@ -194,11 +185,13 @@ app.listen(port, () => console.log([ `Listening on port │ ${port}`, `Debug mode │ ${debug}`, + `Mocked data │ ${conf.settings.mocked ?? false}`, `Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`, `Cached time │ ${cached} seconds`, `Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`, `Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`, `Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`, + `SVG optimization │ ${conf.settings.optimize ?? false}`, `Server ready !` ].join("\n"))) } diff --git a/source/app/web/statics/app.js b/source/app/web/statics/app.js index e57ac3a8..fbe8c0c9 100644 --- a/source/app/web/statics/app.js +++ b/source/app/web/statics/app.js @@ -1,6 +1,5 @@ ;(async function() { //Init - const url = new URLSearchParams(window.location.search) const {data:templates} = await axios.get("/.templates") const {data:plugins} = await axios.get("/.plugins") const {data:base} = await axios.get("/.plugins.base") @@ -10,12 +9,27 @@ //Initialization el:"main", async mounted() { - //Load instance - await this.load() //Interpolate config from browser try { this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") } catch (error) {} + //GitHub limit tracker + const {data:requests} = await axios.get("/.requests") + this.requests = requests + setInterval(async () => { + const {data:requests} = await axios.get("/.requests") + this.requests = requests + }, 15000) + //Generate placeholder + this.mock({timeout:200}) + setInterval(() => { + const marker = document.querySelector("#metrics-end") + if (marker) { + this.mockresize() + marker.remove() + } + }, 100) }, components:{Prism:PrismComponent}, //Watchers @@ -31,12 +45,14 @@ //Data initialization data:{ version, - user:url.get("user") || "", + user:"", tab:"overview", - palette:url.get("palette") || (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") || "light", + palette:"light", requests:{limit:0, used:0, remaining:0, reset:0}, + cached:new Map(), config:{ timezone:"", + animated:true, }, plugins:{ base, @@ -59,23 +75,47 @@ stars:"🌟 Recently starred repositories", stargazers:"✨ Stargazers over last weeks", activity:"📰 Recent activity", - "base.header":` - - Header`, - "base.activity":` - - Account activity`, - "base.community":` - - Community stats`, - "base.repositories":` - - Repositories metrics`, - "base.metadata":` - - Metadata`, + people:"🧑‍🤝‍🧑 Followers and followed", + base:"🗃️ Base content", + "base.header":"Header", + "base.activity":"Account activity", + "base.community":"Community stats", + "base.repositories":"Repositories metrics", + "base.metadata":"Metadata", }, options:{ + descriptions:{ + "languages.ignored":{text:"Ignored languages", placeholder:"lang-0, lang-1, ..."}, + "languages.skipped":{text:"Skipped repositories", placeholder:"repo-0, repo-1, ..."}, + "pagespeed.detailed":{text:"Detailed audit", type:"boolean"}, + "pagespeed.screenshot":{text:"Audit screenshot", type:"boolean"}, + "pagespeed.url":{text:"Url", placeholder:"(default to GitHub attached)"}, + "habits.from":{text:"Events to use", type:"number", min:1, max:1000}, + "habits.days":{text:"Max events age", type:"number", min:1, max:30}, + "habits.facts":{text:"Display facts", type:"boolean"}, + "habits.charts":{text:"Display charts", type:"boolean"}, + "music.playlist":{text:"Playlist url", placeholder:"https://embed.music.apple.com/en/playlist/"}, + "music.limit":{text:"Limit", type:"number", min:1, max:100}, + "posts.limit":{text:"Limit", type:"number", min:1, max:30}, + "posts.user":{text:"Username", placeholder:"(default to GitHub login)"}, + "posts.source":{text:"Source", type:"select", values:["dev.to"]}, + "isocalendar.duration":{text:"Duration", type:"select", values:["half-year", "full-year"]}, + "projects.limit":{text:"Limit", type:"number", min:0, max:100}, + "projects.repositories":{text:"Repositories projects", placeholder:"user/repo/projects/1, ..."}, + "topics.mode":{text:"Mode", type:"select", values:["starred", "mastered"]}, + "topics.sort":{text:"Sort by", type:"select", values:["starred", "activity", "stars", "random"]}, + "topics.limit":{text:"Limit", type:"number", min:0, max:20}, + "tweets.limit":{text:"Limit", type:"number", min:1, max:10}, + "tweets.user":{text:"Username", placeholder:"(default to GitHub attached)"}, + "stars.limit":{text:"Limit", type:"number", min:1, max:100}, + "activity.limit":{text:"Limit", type:"number", min:1, max:100}, + "activity.days":{text:"Max events age", type:"number", min:1, max:9999}, + "activity.filter":{text:"Events type", placeholder:"all"}, + "people.size":{text:"Limit", type:"number", min:16, max:64}, + "people.limit":{text:"Limit", type:"number", min:1, max:9999}, + "people.types":{text:"Types", placeholder:"followers, following"}, + "people.identicons":{text:"Use identicons", type:"boolean"}, + }, "languages.ignored":"", "languages.skipped":"", "pagespeed.detailed":false, @@ -87,6 +127,7 @@ "music.playlist":"", "music.limit":4, "posts.limit":4, + "posts.user":"", "posts.source":"dev.to", "isocalendar.duration":"half-year", "projects.limit":4, @@ -95,15 +136,24 @@ "topics.sort":"stars", "topics.limit":12, "tweets.limit":2, + "tweets.user":"", "stars.limit":4, "activity.limit":5, + "activity.days":14, + "activity.filter":"all", + "people.size":28, + "people.limit":28, + "people.types":"followers, following", + "people.identicons":false, }, }, templates:{ list:templates, - selected:url.get("template") || templates[0].name, - loaded:{}, - placeholder:"", + selected:templates[0]?.name||"classic", + placeholder:{ + timeout:null, + image:"" + }, descriptions:{ classic:"Classic template", terminal:"Terminal template", @@ -120,7 +170,7 @@ computed:{ //User's avatar avatar() { - return `https://github.com/${this.user}.png` + return this.generated.content ? `https://github.com/${this.user}.png` : null }, //User's repository repo() { @@ -158,16 +208,18 @@ `on:`, ` # Schedule updates`, ` schedule: [{cron: "0 * * * *"}]`, - ` push: {branches: "master"}`, + ` # Lines below let you run workflow manually and on each commit`, + ` push: {branches: ["master", "main"]}`, + ` workflow_dispatch:`, `jobs:`, ` github-metrics:`, ` runs-on: ubuntu-latest`, ` steps:`, ` - uses: lowlighter/metrics@latest`, ` with:`, - ` # You'll need to setup a personal token in your secrets.`, + ` # Your GitHub token`, ` token: ${"$"}{{ secrets.METRICS_TOKEN }}`, - ` # GITHUB_TOKEN is a special auto-generated token used for commits`, + ` # GITHUB_TOKEN is a special auto-generated token restricted to current repository, which is used to push files in it`, ` committer_token: ${"$"}{{ secrets.GITHUB_TOKEN }}`, ``, ` # Options`, @@ -180,18 +232,40 @@ ...Object.entries(this.config).filter(([key, value]) => value).map(([key, value]) => ` config_${key.replace(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`), ].sort(), ].join("\n") + }, + //Configurable plugins + configure() { + //Check enabled plugins + const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value)&&(key !== "base")).map(([key, value]) => key) + const filter = new RegExp(`^(?:${enabled.join("|")})[.]`) + //Search related options + const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => filter.test(key)) + entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]])) + entries.sort((a, b) => a[0].localeCompare(b[0])) + //Return object + const configure = Object.fromEntries(entries) + return Object.keys(configure).length ? configure : null } }, //Methods methods:{ - //Load and render image - async load() { - //Render placeholder - const url = this.url.replace(new RegExp(`${this.user}(\\?|$)`), "placeholder$1") - this.templates.placeholder = this.serialize((await axios.get(url)).data) + //Load and render placeholder image + async mock({timeout = 600} = {}) { + clearTimeout(this.templates.placeholder.timeout) + this.templates.placeholder.timeout = setTimeout(async () => { + this.templates.placeholder.image = await placeholder(this) this.generated.content = "" - //Start GitHub rate limiter tracker - this.ghlimit() + this.generated.error = false + }, timeout) + }, + //Resize mock image + mockresize() { + const svg = document.querySelector(".preview .image svg") + if (svg) { + const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y-svg.getBoundingClientRect()?.y + if (Number.isFinite(height)) + svg.setAttribute("height", height) + } }, //Generate metrics and flush cache async generate() { @@ -202,26 +276,15 @@ //Compute metrics try { await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`) - this.generated.content = this.serialize((await axios.get(this.url)).data) + this.generated.content = (await axios.get(this.url)).data + this.generated.error = false } catch { this.generated.error = true } finally { this.generated.pending = false } - this.ghlimit({once:true}) }, - //Serialize svg - serialize(svg) { - return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}` - }, - //Update reate limit requests - async ghlimit({once = false} = {}) { - const {data:requests} = await axios.get("/.requests") - this.requests = requests - if (!once) - setTimeout(() => this.ghlimit(), 30*1000) - } }, }) })() \ No newline at end of file diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js new file mode 100644 index 00000000..d4280c40 --- /dev/null +++ b/source/app/web/statics/app.placeholder.js @@ -0,0 +1,530 @@ +(function () { + //Load asset + const cached = new Map() + async function load(url) { + if (!cached.has(url)) + cached.set(url, (await axios.get(url)).data) + return cached.get(url) + } + //Distribution function + function distribution(length) { + let probability = 1 + const values = [] + for (let i = 0; i < length-1; i++) { + const value = Math.random()*probability + values.push(value) + probability -= value + } + values.push(probability) + return values.sort((a, b) => b - a) + } + //Placeholder function + window.placeholder = async function (set) { + //Load templates informations + let {image, style, fonts, partials} = await load(`/.templates/${set.templates.selected}`) + await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${partial}.ejs`))) + //Trap includes + image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`) + //Faked data + const options = set.plugins.options + const data = { + //Template elements + style, fonts, errors:[], + partials:new Set(partials), + //Plural helper + s(value, end = "") { + return value !== 1 ? {y:"ies", "":"s"}[end] : end + }, + //Formatter helper + f(n, {sign = false} = {}) { + for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}]) + if (n/v >= 1) + return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}` + return `${(sign)&&(n > 0) ? "+" : ""}${n}` + }, + //Trap for includes + async $include(path) { + const partial = await load(`/.templates/${set.templates.selected}/${path}`) + return await ejs.render(partial, data, {async:true, rmWhitespace:true}) + }, + //Meta-data + meta:{version:set.version, author:"lowlighter"}, + //Animated + animated:set.config.animated, + //Config + config:set.config, + //Base elements + base:set.plugins.enabled.base, + //Computed elements + computed: { + commits:faker.random.number(10000), + sponsorships:faker.random.number(10), + licenses:{favorite:[""], used:{MIT:1}}, + token:{scopes:[]}, + repositories: { + watchers:faker.random.number(1000), + stargazers:faker.random.number(10000), + issues_open:faker.random.number(1000), + issues_closed:faker.random.number(1000), + pr_open:faker.random.number(1000), + pr_merged:faker.random.number(1000), + forks:faker.random.number(1000), + releases:faker.random.number(1000), + }, + diskUsage:`${faker.random.float({min:1, max:999}).toFixed(1)}MB`, + registration:`${faker.random.number({min:2, max:10})} years ago`, + cakeday:false, + calendar:new Array(14).fill(null).map(_ => ({color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])})), + avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" + }, + //User data + user:{ + databaseId:faker.random.number(10000000), + name:"(placeholder)", + login:set.user||"metrics", + createdAt:`${faker.date.past(10)}`, + avatarUrl:set.avatar, + websiteUrl:options["pagespeed.url"]||"(attached website)", + isHireable:false, + twitterUsername:options["tweets.user"]||"(attached Twitter account)", + repositories:{totalCount:faker.random.number(100), totalDiskUsage:faker.random.number(100000), nodes:[]}, + packages:{totalCount:faker.random.number(10)}, + starredRepositories:{totalCount:faker.random.number(1000)}, + watching:{totalCount:faker.random.number(100)}, + sponsorshipsAsSponsor:{totalCount:faker.random.number(10)}, + sponsorshipsAsMaintainer:{totalCount:faker.random.number(10)}, + contributionsCollection:{ + totalRepositoriesWithContributedCommits:faker.random.number(100), + totalCommitContributions:faker.random.number(10000), + restrictedContributionsCount:faker.random.number(10000), + totalIssueContributions:faker.random.number(100), + totalPullRequestContributions:faker.random.number(1000), + totalPullRequestReviewContributions:faker.random.number(1000), + }, + calendar:{contributionCalendar:{weeks:[]}}, + repositoriesContributedTo:{totalCount:faker.random.number(100)}, + followers:{totalCount:faker.random.number(1000)}, + following:{totalCount:faker.random.number(1000)}, + issueComments:{totalCount:faker.random.number(1000)}, + organizations:{totalCount:faker.random.number(10)} + }, + //Plugins + plugins:{ + //Tweets + ...(set.plugins.enabled.tweets ? ({ + tweets:{ + username:options["tweets.user"]||"(attached Twitter account)", + profile:{ + profile_image_url:faker.image.people(), + name:"", + verified:false, + id:faker.random.number(1000000).toString(), + username:options["tweets.user"]||"(attached Twitter account)", + profile_image:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + }, + list:[ + { + id:faker.random.number(100000000000000).toString(), + created_at:faker.date.recent(), + entities: { + mentions: [ { start: 22, end: 33, username: 'lowlighter' } ] + }, + text: 'Checkout metrics from @lowlighter ! #GitHub ', + mentions: [ 'lowlighter' ] + }, + ...new Array(Number(options["tweets.limit"])-1).fill(null).map(_ => ({ + id:faker.random.number(100000000000000).toString(), + created_at:faker.date.recent(), + text:faker.lorem.paragraph(), + mentions:[] + })), + ] + } + }) : null), + //Lines + ...(set.plugins.enabled.lines ? ({ + lines:{ + added:`${faker.random.number(100)}.${faker.random.number(9)}k`, + deleted:`${faker.random.number(100)}.${faker.random.number(9)}k`, + } + }) : null), + //Traffic + ...(set.plugins.enabled.traffic ? ({ + traffic:{ + views:{ + count:`${faker.random.number({min:10, max:100})}.${faker.random.number(9)}k`, + uniques:`${faker.random.number(10)}.${faker.random.number(9)}k`, + } + } + }) : null), + //Follow-up + ...(set.plugins.enabled.followup ? ({ + followup:{ + issues:{get count() { return this.open + this.closed }, open:faker.random.number(1000), closed:faker.random.number(1000)}, + pr:{get count() { return this.open + this.merged }, open:faker.random.number(1000), merged:faker.random.number(1000)}, + } + }) : null), + //Gists + ...(set.plugins.enabled.gists ? ({ + gists:{ + totalCount:faker.random.number(100), + stargazers:faker.random.number(1000), + forks:faker.random.number(100), + files:faker.random.number(100), + comments:faker.random.number(1000) + } + }) : null), + //Languages + ...(set.plugins.enabled.languages ? ({ + languages:{ + get colors() { return Object.fromEntries(Object.entries(this.favorites).map(([key, {color}]) => [key, color])) }, + total:faker.random.number(10000), + get stats() { return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value])) }, + favorites:distribution(7).map((value, index, array) => ({name:faker.lorem.word(), color:faker.internet.color(), value, x:array.slice(0, index).reduce((a, b) => a + b, 0)})) + } + }) : null), + //Habits + ...(set.plugins.enabled.habits ? ({ + habits:{ + facts:options["habits.facts"], + charts:options["habits.charts"], + commits:{ + get hour() { return Object.keys(this.hours).filter(key => /^\d+$/.test(key)).map(key => [key, this.hours[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0] }, + hours:{ + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + [faker.random.number(24)]:faker.random.number(10), + get max() { return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] } + }, + get day() { return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.keys(this.days).filter(key => /^\d+$/.test(key)).map(key => [key, this.days[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0]] }, + days:{ + "0":faker.random.number(10), + "1":faker.random.number(10), + "2":faker.random.number(10), + "3":faker.random.number(10), + "4":faker.random.number(10), + "5":faker.random.number(10), + "6":faker.random.number(10), + get max() { return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] } + }, + }, + indents:{style:"spaces", spaces:1, tabs:0}, + linguist:{ + available:true, + get ordered() { return Object.entries(this.languages) }, + get languages() { return Object.fromEntries(distribution(4).map(value => [faker.lorem.word(), value])) }, + } + } + }) : null), + //People + ...(set.plugins.enabled.people ? ({ + people:{ + types:options["people.types"].split(",").map(x => x.trim()), + size:options["people.size"], + followers:new Array(Number(options["people.limit"])).fill(null).map(_ => ({ + login:faker.internet.userName(), + avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + following:new Array(Number(options["people.limit"])).fill(null).map(_ => ({ + login:faker.internet.userName(), + avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })) + } + }) : null), + //Music + ...(set.plugins.enabled.music ? ({ + music:{ + provider:"(music provider)", + mode:"Suggested tracks", + tracks:new Array(Number(options["music.limit"])).fill(null).map(_ => ({ + name:faker.random.words(5), + artist:faker.random.words(), + artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })) + } + }) : null), + //Pagespeed + ...(set.plugins.enabled.pagespeed ? ({ + pagespeed:{ + url:options["pagespeed.url"]||"(attached website url)", + detailed:options["pagespeed.detailed"]||false, + scores: [ + {score:faker.random.float({max:1}), title:"Performance"}, + {score:faker.random.float({max:1}), title:"Accessibility"}, + {score:faker.random.float({max:1}), title:"Best Practices"}, + {score:faker.random.float({max:1}), title:"SEO"} + ], + metrics:{ + observedFirstContentfulPaint:faker.random.number(500), + observedFirstVisualChangeTs:faker.time.recent(), + observedFirstContentfulPaintTs:faker.time.recent(), + firstContentfulPaint:faker.random.number(500), + observedDomContentLoaded:faker.random.number(500), + observedFirstMeaningfulPaint:faker.random.number(1000), + maxPotentialFID:faker.random.number(500), + observedLoad:faker.random.number(500), + firstMeaningfulPaint:faker.random.number(500), + observedCumulativeLayoutShift:faker.random.float({max:1}), + observedSpeedIndex:faker.random.number(1000), + observedSpeedIndexTs:faker.time.recent(), + observedTimeOriginTs:faker.time.recent(), + observedLargestContentfulPaint:faker.random.number(1000), + cumulativeLayoutShift:faker.random.float({max:1}), + observedFirstPaintTs:faker.time.recent(), + observedTraceEndTs:faker.time.recent(), + largestContentfulPaint:faker.random.number(2000), + observedTimeOrigin:faker.random.number(10), + speedIndex:faker.random.number(1000), + observedTraceEnd:faker.random.number(2000), + observedDomContentLoadedTs:faker.time.recent(), + observedFirstPaint:faker.random.number(500), + totalBlockingTime:faker.random.number(500), + observedLastVisualChangeTs:faker.time.recent(), + observedFirstVisualChange:faker.random.number(500), + observedLargestContentfulPaintTs:faker.time.recent(), + estimatedInputLatency:faker.random.number(100), + observedLoadTs:faker.time.recent(), + observedLastVisualChange:faker.random.number(1000), + firstCPUIdle:faker.random.number(1000), + interactive:faker.random.number(1000), + observedNavigationStartTs:faker.time.recent(), + observedNavigationStart:faker.random.number(10), + observedFirstMeaningfulPaintTs:faker.time.recent() + }, + screenshot:options["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null + } + }) : null), + //Projects + ...(set.plugins.enabled.projects ? ({ + projects:{ + totalCount:options["projects.limit"]+faker.random.number(10), + list:new Array(Number(options["projects.limit"])).fill(null).map(_ => ({ + name:faker.lorem.sentence(), + updated:`${2+faker.date.recent(8)} days ago`, + progress:{enabled:true, todo:faker.random.number(50), doing:faker.random.number(50), done:faker.random.number(50), get total() { return this.todo + this.doing + this.done } } + })) + } + }) : null), + //Posts + ...(set.plugins.enabled.posts ? ({ + posts:{ + source:options["posts.source"], + list:new Array(Number(options["posts.limit"])).fill(null).map(_ => ({ + title:faker.lorem.sentence(), + date:faker.date.recent().toString().substring(4, 10).trim() + })) + } + }) : null), + //Topics + ...(set.plugins.enabled.topics ? ({ + topics:{ + mode:options["topics.mode"], + list:new Array(Number(options["topics.limit"])||20).fill(null).map(_ => ({ + name:faker.lorem.words(2), + description:faker.lorem.sentence(), + icon:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" + })) + } + }) : null), + //Stars + ...(set.plugins.enabled.stars ? ({ + stars:{ + repositories: [ + { + starredAt:faker.date.recent(), + node: { + description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !", + forkCount:faker.random.number(100), + isFork:false, + issues:{ + totalCount:faker.random.number(100), + }, + nameWithOwner:"lowlighter/metrics", + openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a", + pullRequests:{ + totalCount:faker.random.number(100), + }, + stargazerCount:faker.random.number(10000), + licenseInfo:{nickname:null, name:"MIT License"}, + primaryLanguage:{color:"#f1e05a", name:"JavaScript"} + }, + starred:"1 day ago" + }, + ...new Array(Number(options["stars.limit"])-1).fill(null).map((_, i) => ({ + starredAt:faker.date.recent(), + node: { + description:faker.lorem.sentence(), + forkCount:faker.random.number(100), + isFork:faker.random.boolean(), + issues:{ + totalCount:faker.random.number(100), + }, + nameWithOwner:`${faker.random.word()}/${faker.random.word()}`, + openGraphImageUrl:faker.internet.url(), + pullRequests:{ + totalCount:faker.random.number(100), + }, + stargazerCount:faker.random.number(10000), + licenseInfo:{nickname:null, name:"License"}, + primaryLanguage:{color:faker.internet.color(), name:faker.lorem.word()} + }, + starred:`${i+2} days ago` + })), + ] + } + }) : null), + //Stars + ...(set.plugins.enabled.stargazers ? ({ + get stargazers() { + const dates = [] + let total = faker.random.number(1000) + const result = { + total:{ + dates:{}, + get max() { return Math.max(...dates.map(date => this.dates[date])) }, + get min() { return Math.min(...dates.map(date => this.dates[date])) }, + }, + increments:{ + dates:{}, + get max() { return Math.max(...dates.map(date => this.dates[date])) }, + get min() { return Math.min(...dates.map(date => this.dates[date])) }, + }, + months:["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."] + } + for (let d = -14; d <= 0; d++) { + const date = new Date(Date.now()-d*24*60*60*1000).toISOString().substring(0, 10) + dates.push(date) + result.total.dates[date] = (total += (result.increments.dates[date] = faker.random.number(100))) + } + return result + } + }) : null), + //Activity + ...(set.plugins.enabled.activity ? ({ + activity:{ + events:new Array(Number(options["activity.limit"])).fill(null).map(_ => [ + { + type:"push", + repo:`${faker.random.word()}/${faker.random.word()}`, + size:1, + branch:"master", + commits: [ { sha:faker.git.shortSha(), message:faker.lorem.sentence()} ] + }, + { + type:"comment", + on:"commit", + repo:`${faker.random.word()}/${faker.random.word()}`, + content:faker.lorem.paragraph(), + user:set.user, + mobile:null, + number:faker.git.shortSha(), + title:"", + }, + { + type:"comment", + on:"pr", + repo:`${faker.random.word()}/${faker.random.word()}`, + content:faker.lorem.sentence(), + user:set.user, + mobile:null, + number:faker.random.number(100), + title:faker.lorem.paragraph(), + }, + { + type:"comment", + on:"issue", + repo:`${faker.random.word()}/${faker.random.word()}`, + content:faker.lorem.sentence(), + user:set.user, + mobile:null, + number:faker.random.number(100), + title:faker.lorem.paragraph(), + }, + { + type:"issue", + repo:`${faker.random.word()}/${faker.random.word()}`, + action:faker.random.arrayElement(["opened", "closed", "reopened"]), + user:set.user, + number:faker.random.number(100), + title:faker.lorem.paragraph(), + }, + { + type:"pr", + repo:`${faker.random.word()}/${faker.random.word()}`, + action:faker.random.arrayElement(["opened", "closed"]), + user:set.user, + number:faker.random.number(100), + title:faker.lorem.paragraph(), + lines:{added:faker.random.number(1000), deleted:faker.random.number(1000)}, files:{changed:faker.random.number(10)} + }, + { + type:"wiki", + repo:`${faker.random.word()}/${faker.random.word()}`, + pages:[faker.lorem.sentence(), faker.lorem.sentence()] + }, + { + type:"fork", + repo:`${faker.random.word()}/${faker.random.word()}`, + }, + { + type:"review", + repo:`${faker.random.word()}/${faker.random.word()}`, + user:set.user, + number:faker.random.number(100), + title:faker.lorem.paragraph(), + }, + { + type:"release", + repo:`${faker.random.word()}/${faker.random.word()}`, + action:"published", + name:faker.random.words(4), + draft:faker.random.boolean(), + prerelease:faker.random.boolean(), + }, + { + type:"ref/create", + repo:`${faker.random.word()}/${faker.random.word()}`, + ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"]),} + }, + { + type:"ref/delete", + repo:`${faker.random.word()}/${faker.random.word()}`, + ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"]),} + }, + { + type:"member", + repo:`${faker.random.word()}/${faker.random.word()}`, + user:set.user + }, + { + type:"public", + repo:`${faker.random.word()}/${faker.random.word()}`, + }, + { + type:"star", + repo:`${faker.random.word()}/${faker.random.word()}`, + action:"started" + }, + ][Math.floor(Math.random()*15)]) + } + }) : null), + //Isocalendar + ...(set.plugins.enabled.isocalendar ? ({ + isocalendar:{ + streak:{max:30+faker.random.number(20), current:faker.random.number(30)}, + max:10+faker.random.number(40), + average:faker.random.float(10), + svg:"(isometric calendar is not displayed in placeholder)", + duration:options["isocalendar.duration"] + } + }) : null), + }, + } + //Render + return await ejs.render(image, data, {async:true, rmWhitespace:true}) + } +})() diff --git a/source/app/web/statics/index.html b/source/app/web/statics/index.html index f449f21f..e1ec79a7 100644 --- a/source/app/web/statics/index.html +++ b/source/app/web/statics/index.html @@ -16,289 +16,112 @@
@@ -307,8 +130,10 @@ + + \ No newline at end of file diff --git a/source/app/web/statics/style.css b/source/app/web/statics/style.css index 86ba3635..ec7ebdc3 100644 --- a/source/app/web/statics/style.css +++ b/source/app/web/statics/style.css @@ -1,147 +1,159 @@ -/* General */ - body { - font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; - padding: 0; - margin: 0; - background-color: var(--color-bg-canvas); - } - main { - height: auto; - color: var(--color-text-primary); - background-color: var(--color-bg-canvas); - display: flex; - flex-direction: column; - } - .flex { - display: flex; - } -/* Title */ - .title { - margin: 0; - padding: 1rem 2rem; +body { + font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; + margin: 0; + padding: 0; + background-color: var(--color-bg-canvas); + color: var(--color-text-primary); +} + +/* Header */ + header { display: flex; align-items: center; - font-size: 1.6rem; + padding: 1rem; + color: var(--color-header-text); background-color: var(--color-header-bg); } - .title a { - color: #fff !important; - font-weight: normal; - } - .title svg { - margin-right: 2rem; - fill: #fff !important; - width: 2rem; - height: 2rem; - } -/* Tabs */ - nav { - display: none; - border-bottom: 1px solid var(--color-border-secondary); - flex-shrink: 0; - overflow-x: auto; - } - nav .tab { - flex-shrink: 0; - display: flex; - align-items: center; - padding: 8px 16px; - font-size: 14px; - line-height: 30px; - color: var(--color-underlinenav-text-hover); - cursor: pointer; - } - nav .tab.active { - color: var(--color-underlinenav-text-active); - border-bottom: 2px solid #f9826c; - font-weight: 600; - } - nav .tab.disabled { - opacity: .5; - cursor: not-allowed; - pointer-events: none; - } - nav .tab svg { + + header svg { fill: currentColor; - margin-right: .5rem; + margin-right: 1rem; + height: 2rem; + width: 2rem; } - nav .right { + + header a, header a:visited { + color: inherit; + text-decoration: none; + font-weight: 600; + font-size: 1.5rem; + } + +/* Interface */ + .ui { display: flex; - border: none; + flex-direction: column; } - nav .left { + + .ui > * { + margin: 1rem; + } + + .ui.top { + border-bottom: 1px solid var(--color-border-secondary); + } + .ui.top > * { + margin: 0 1rem; + } + .ui.top aside { display: none; - } -/* Readme container */ - .container { - max-width: 1280px; - display: flex; - flex-grow: 1; - height: 100%; - flex-direction: column; - } - .left, .right { - margin: 0 8px 16px; - } - .left { - flex-shrink: 0; - margin: 0; width: 100%; - min-width: 230px; - display: flex; - flex-direction: column; } - .left .user { - flex-shrink: 0; - display: flex; - flex-direction: column; - } - .left .user input, .left .user button { - width: 100%; - margin: 4px 0; - } - .left .scrollable { - flex-grow: 1; - overflow: auto; - margin-bottom: 1rem; - } - .right { - flex-grow: 1; - border-radius: 6px; - border: 1px solid var(--color-border-primary); - width: auto; - } - .right .body { - margin: 24px; - } -/* Avatar */ - .avatar { + + .ui-avatar { display: none; - justify-content: center; - margin-top: -20%; - z-index: 888; - } - .avatar div { - width: 50%; - padding-top: 50%; + width: 70%; + padding-top: 70%; border-radius: 50%; box-shadow: 0 0 0 1px var(--color-avatar-border); border: 1px solid var(--color-border-primary); background-color: black; background-size: cover; + margin: -3rem auto 1rem; } + +/* Tabs */ + nav { + display: flex; + align-items: center; + padding-top: 1.5rem; + overflow: auto; + } + + nav svg { + fill: currentColor; + margin-right: .5rem; + } + + nav > div { + display: flex; + align-items: center; + flex-shrink: 0; + padding: .5rem 1rem; + color: var(--color-underlinenav-text-hover); + cursor: pointer; + } + + nav > div.active { + color: var(--color-underlinenav-text-active); + border-bottom: 2px solid var(--color-underlinenav-border-active); + font-weight: 600; + } + + nav > div.disabled { + opacity: .5; + cursor: not-allowed; + } + + nav > div:hover { + color: var(--color-underlinenav-text-hover); + border-bottom: 2px solid var(--color-underlinenav-border-hover); + transition-duration: all .12s ease-out; + } + +/* Configuration */ + aside { + display: flex; + flex-direction: column; + flex-shrink: 0; + } + + .configuration { + display: flex; + flex-direction: column; + padding-top: 1rem; + margin: 1rem .5rem 0; + border-top: 1px solid var(--color-border-primary); + } + + .option { + display: flex; + flex-direction: column; + } + +/* Preview */ + .preview { + border: 1px solid var(--color-border-primary); + border-radius: 6px; + padding: 1rem; + flex-grow: 1; + overflow: auto; + } + + .preview .image { + overflow: auto; + } + + .preview .image.pending { + opacity: .25; + } + + .code pre { + border-radius: 6px; + } + /* Readme */ .readme { display: flex; align-items: center; font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; - font-size: 12px; - margin-bottom: 16px; + font-size: .75rem; + margin-bottom: 1rem; color: var(--color-text-primary); } .readme svg { fill: currentColor; - margin-right: 8px; + margin-right: .5rem; } .readme .slash { padding: 0 2px; @@ -149,168 +161,84 @@ .readme .md { color: var(--color-text-tertiary); } -/* Readme content */ - .right section { - height: 87%; - overflow: auto; - } -/* Code */ - .code { - overflow-x: auto; - width: 100%; - } - .code pre { - border-radius: 5px; - } -/* Plugins */ - .plugins, .templates { - display: flex; - flex-direction: column; - } - .plugins label, .templates label { - margin: 0; - display: flex; - align-items: center; - } - .plugins label svg, .templates label svg { - fill: currentColor; - } - .options { - display: flex; - flex-direction: column; - } - .options-group { - display: flex; - flex-direction: column; - } - .options-group label { - margin: 0; - display: flex; - flex-direction: column; - } - .options-group h4 { - font-size: 1rem; - font-weight: 600; - margin: 0; - } -/* Step */ - .step { - padding: 1rem .5rem; - border-bottom: 1px solid var(--color-border-secondary); - } - .step h2 { - margin: 0; - margin-bottom: .25rem; - font-weight: 600; - font-size: 1.2rem; - } -/* Links */ - a, a:hover, a:visited { - color: var(--color-text-link); - text-decoration: none; - font-style: normal; - outline: none; - } - a:hover { - text-decoration: underline; - transition: color .4s; + +/* Inputs */ + label { cursor: pointer; } -/* Inputs */ - input, button, select { + label:hover { background-color: var(--color-input-contrast-bg); - padding: 5px 12px; - font-size: 14px; - line-height: 20px; + border-radius: 6px; + } + + input[type=text], input[type=number], select { + background-color: var(--color-input-contrast-bg); + padding: .4rem .8rem; color: var(--color-text-primary); + background-color: var(--color-input-bg); border: 1px solid var(--color-input-border); border-radius: 6px; outline: none; box-shadow: var(--color-shadow-inset); - cursor: pointer; } + + input[type=text]:focus, input[type=number]:focus, select:focus { + background-color: var(--color-input-bg); + border-color: var(--color-state-focus-border); + outline: none; + box-shadow: var(--color-state-focus-shadow); + } + button { color: var(--color-btn-primary-text); background-color: var(--color-btn-primary-bg); border-color: var(--color-btn-primary-border); + box-shadow: var(--color-btn-primary-shadow),var(--color-btn-primary-inset-shadow); + padding: .4rem .8rem; + border-radius: 6px; + font-weight: 500; + margin: .5rem 0; + cursor: pointer; + transition-duration: all .12s ease-out; } - input:focus { + + button[disabled] { + color: var(--color-btn-primary-disabled-text); + background-color: var(--color-btn-primary-disabled-bg); + border-color: var(--color-btn-primary-disabled-border); + } + + button:focus { outline: none; } - label, button { - margin: 1rem; + +/* Links */ + a, a:hover, a:visited { + color: var(--color-text-link); + text-decoration: none; } - label { - padding-right: .25rem; - padding-bottom: .125rem; + + a:hover { + text-decoration: underline; } - input[disabled], button[disabled], select[disabled] { - opacity: .5; - cursor: not-allowed; - } - label:hover { - border-radius: .25rem; - background-color: #79B8FF50; - transition: background-color .4s; - cursor: pointer; - } - .not-available { - opacity: .3; - } -/* Error */ - .error { - color: #721c24; - background-color: #f8d7da; - padding: .75rem 1.25rem; - border: 1px solid #f5c6cb; - border-radius: .25rem; - display: flex; - justify-content: center; - align-items: center; - } -/* Github requests */ - .gh-requests { - font-size: .8rem; - } -/* Metrics */ - .metrics { - width: 100%; - max-width: 480px; + +/* Requests */ + small { + text-align: center; } /* Media screen */ - @media only screen and (min-width: 700px) { - .left, .right { - height: 75%; - width: 0%; - } - .left { - width: 25%; - margin: 0 8px; - } - nav { - margin: 32px 0 24px; - overflow: hidden; - display: flex; - } - nav .left { - display: block; - } - nav .right { - height: 100%; - } - .container { + @media only screen and (min-width: 740px) { + .ui { flex-direction: row; } - main { - height: 100vh; - width: 100vw; - overflow: hidden; + .ui.top aside { + display: block; } - .avatar { - display: flex; + .ui-avatar { + display: block; } - .mobile { - display: none; + aside { + max-width: 25%; } - } + } \ No newline at end of file diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index 0693ca21..4801e158 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -24,7 +24,7 @@ .filter(({actor}) => actor.login === login) .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now()-days*24*60*60*1000) : true) .map(({type, payload, repo:{name:repo}}) => { - //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types#memberevent + //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types switch (type) { //Commented on a commit case "CommitCommentEvent":{ diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs index 18a38d9e..f6ae8841 100644 --- a/source/plugins/people/index.mjs +++ b/source/plugins/people/index.mjs @@ -40,7 +40,7 @@ } //Convert avatars to base64 console.debug(`metrics/compute/${login}/plugins > people > loading avatars`) - await Promise.all(result[type].map(async user => user.avatar = await imports.imgb64(user.avatarUrl))) + await Promise.all(result[type].map(async user => user.avatar = user.avatarUrl ? await imports.imgb64(user.avatarUrl) : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==")) } //Results diff --git a/source/templates/classic/image.svg b/source/templates/classic/image.svg index 67f6431d..5e9b5755 100644 --- a/source/templates/classic/image.svg +++ b/source/templates/classic/image.svg @@ -6,7 +6,7 @@
<% for (const partial of [...partials]) { %> - <%- await include(`partials/${partial}.ejs`); %> + <%- await include(`partials/${partial}.ejs`) %> <% } %> <% if (base.metadata) { %> diff --git a/source/templates/classic/partials/people.ejs b/source/templates/classic/partials/people.ejs index 9686e488..4b991d0b 100644 --- a/source/templates/classic/partials/people.ejs +++ b/source/templates/classic/partials/people.ejs @@ -3,7 +3,7 @@

- + People

@@ -29,7 +29,7 @@ <%= plugins.people.error.message %>
<% } else { %> - <% for (const user of plugins.people.followers) { %><% } %> + <% for (const user of plugins.people.followers) { %><% } %> <% } %>
@@ -49,7 +49,7 @@ <%= plugins.people.error.message %> <% } else { %> - <% for (const user of plugins.people.following) { %><% } %> + <% for (const user of plugins.people.following) { %><% } %> <% } %> diff --git a/source/templates/repository/image.svg b/source/templates/repository/image.svg index 6a330f8f..29b9debb 100644 --- a/source/templates/repository/image.svg +++ b/source/templates/repository/image.svg @@ -20,7 +20,7 @@ <% } else { %> <% for (const partial of [...partials]) { %> - <%- await include(`partials/${partial}.ejs`); %> + <%- await include(`partials/${partial}.ejs`) %> <% } %> <% } %> diff --git a/source/templates/terminal/image.svg b/source/templates/terminal/image.svg index 78cdd366..10fafed1 100644 --- a/source/templates/terminal/image.svg +++ b/source/templates/terminal/image.svg @@ -50,7 +50,7 @@ WARRANTY, to the extent permitted by applicable law. Last generated: <%= new Date().toGMTString() %> <% } -%> -<% for (const partial of [...partials]) { %><%- await include(`partials/${partial}.ejs`); %><% } -%> +<% for (const partial of [...partials]) { %><%- await include(`partials/${partial}.ejs`) %><% } -%> <% if (base.metadata) { -%>
Connection reset by <%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>
<%# -%> diff --git a/source/templates/terminal/partials/base.header.ejs b/source/templates/terminal/partials/base.header.ejs index 6ad8dd14..d1b51798 100644 --- a/source/templates/terminal/partials/base.header.ejs +++ b/source/templates/terminal/partials/base.header.ejs @@ -1,7 +1,7 @@ <% if (base.header) { %>
<%- meta.$ %> whoami
<%# -%>
<%# -%> -<%= user.name || user.login %> registered=<%= computed.registration.match(/^.+? [ym]/)[0].replace(/ /g, "") %>, uid=<%= `${user.databaseId}`.substr(-4) %>, gid=<%= user.organizations.totalCount %> +<%= user.name || user.login %> registered=<%= computed.registration.match(/^.+? [ymd]/)?.[0].replace(/ /g, "") %>, uid=<%= `${user.databaseId}`.substr(-4) %>, gid=<%= user.organizations.totalCount %> contributed to <%= user.repositoriesContributedTo.totalCount %> repositor<%= s(user.repositoriesContributedTo.totalCount, "y") %> <% for (const [x, {color}] of Object.entries(computed.calendar)) { -%>#<% } %> followed by <%= user.followers.totalCount %> user<%= s(user.followers.totalCount) %>
<% } -%> \ No newline at end of file