Use faker.js for mocked data and placeholder (#47)
This commit is contained in:
69
.github/pull_request_template.md
vendored
69
.github/pull_request_template.md
vendored
@@ -1,35 +1,40 @@
|
||||
<!--
|
||||
Thank you very much for contributing to this repository !
|
||||
|
||||
Link any related issues (if applicable) to help us keeping track of this project current state.
|
||||
Don't forget to run `npm run test` to ensure that you'll pass status checks validation.
|
||||
👋 Hi there!
|
||||
Thanks for contributing to metrics and helping us to improve!
|
||||
|
||||
Please check the following before opening a pull request:
|
||||
- It does not duplicate another existing pull request
|
||||
- It is not mentioned in https://github.com/lowlighter/metrics/projects/1
|
||||
- If it is, ensure that maintainers are aware that you're working on this subject
|
||||
|
||||
Then, explain briefly what your pull request is about and link any related issues (if applicable) to help us keeping track.
|
||||
|
||||
For documentation updates....
|
||||
- Check spelling before asking for a review
|
||||
- Respect current formatting (check that your editions blends well with current state)
|
||||
- Static images must be saved in /.github/readme/imgs and must be of width 1260px
|
||||
- UI should always be set in English in screenshots
|
||||
|
||||
For new plugins...
|
||||
- Ensure that you created:
|
||||
- a plugin entrypoint named index.mjs in /source/plugins
|
||||
- tests in /tests/metrics.test.js
|
||||
- mocked data if needed (required for all APIs which requires a token or limited in requests)
|
||||
- Ensure you updated:
|
||||
- /source/app/action/index.mjs to support new plugin options and use correct typing
|
||||
- /source/web/statics/* to support new plugin options
|
||||
- /settings.example.json with new plugin name
|
||||
- README.md to explain new plugin features
|
||||
- Include a screenshot in your pull request description
|
||||
- You can use `&config.output=png` option in your web instance for it
|
||||
|
||||
For all code editions...
|
||||
- Ensure retro-compatibility with previous versions (
|
||||
- (unless for unreleased features, for which breaking changes are allowed)
|
||||
- Respect current formatting
|
||||
- Prefers using appropriate single words for variables and plugins names
|
||||
- Avoid using uppercases letters, brackets and semicolons when possible to avoid visual pollution
|
||||
- Comments should be added before each "code paragraph" and are considered indent worthy
|
||||
|
||||
-->
|
||||
|
||||
**Pull request description**
|
||||
<!--
|
||||
A clear and concise description of what your pull request implements or fixs.
|
||||
|
||||
Please check the following:
|
||||
|
||||
> Documentation update
|
||||
- Check spelling
|
||||
- Respect current formatting
|
||||
|
||||
> New plugin
|
||||
- Created new plugin in /source/plugins/ with index.mjs as entry point
|
||||
- Added tests in /tests/metrics.test.js
|
||||
- Added mocked data if needed (required for all APIs which requires a token or limited in requests)
|
||||
- Updated action.yml with new plugin options
|
||||
- Updated /source/app/action/index.mjs to retrieve plugin options with correct typing
|
||||
- Updated /source/web/statics/* to support new plugin options
|
||||
- Updated /settings.example.json with new plugin name
|
||||
- Updated README.md to explain plugin features
|
||||
|
||||
> Code editions
|
||||
- Ensure retro-compatibility with previous versions
|
||||
- Unless feature is not released yet
|
||||
- Respect current formatting
|
||||
-->
|
||||
|
||||
**Additional context and screenshots**
|
||||
<!-- Add any other context or screenshots about your pull request here. -->
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]??{})]
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,8 +26,11 @@
|
||||
templates:{},
|
||||
queries:{},
|
||||
settings:{},
|
||||
statics:__statics,
|
||||
node_modules:__modules,
|
||||
paths:{
|
||||
statics:__statics,
|
||||
templates:__templates,
|
||||
node_modules:__modules,
|
||||
}
|
||||
}
|
||||
|
||||
//Load settings
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
@@ -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":`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 8a8 8 0 1116 0v5.25a.75.75 0 01-1.5 0V8a6.5 6.5 0 10-13 0v5.25a.75.75 0 01-1.5 0V8zm5.5 4.25a.75.75 0 01.75-.75h3.5a.75.75 0 010 1.5h-3.5a.75.75 0 01-.75-.75zM3 6.75C3 5.784 3.784 5 4.75 5h6.5c.966 0 1.75.784 1.75 1.75v1.5A1.75 1.75 0 0111.25 10h-6.5A1.75 1.75 0 013 8.25v-1.5zm1.47-.53a.75.75 0 011.06 0l.97.97.97-.97a.75.75 0 011.06 0l.97.97.97-.97a.75.75 0 111.06 1.06l-1.5 1.5a.75.75 0 01-1.06 0L8 7.81l-.97.97a.75.75 0 01-1.06 0l-1.5-1.5a.75.75 0 010-1.06z"></path></svg>
|
||||
Header`,
|
||||
"base.activity":`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
Account activity`,
|
||||
"base.community":`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg>
|
||||
Community stats`,
|
||||
"base.repositories":`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>
|
||||
Repositories metrics`,
|
||||
"base.metadata":`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>
|
||||
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)
|
||||
}
|
||||
},
|
||||
})
|
||||
})()
|
||||
530
source/app/web/statics/app.placeholder.js
Normal file
530
source/app/web/statics/app.placeholder.js
Normal file
@@ -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 <span class="mention">@lowlighter</span> ! <span class="hashtag">#GitHub</span> ',
|
||||
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})
|
||||
}
|
||||
})()
|
||||
@@ -16,289 +16,112 @@
|
||||
<main :class="[palette]">
|
||||
<template>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="title">
|
||||
<a href="https://github.com/lowlighter/metrics">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
||||
Metrics v{{ version }}
|
||||
</a>
|
||||
</h1>
|
||||
<header>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
||||
<a href="https://github.com/lowlighter/metrics">Metrics v{{ version }}</a>
|
||||
</header>
|
||||
|
||||
<!-- Tabs -->
|
||||
<nav>
|
||||
<div class="left"></div>
|
||||
<div class="right">
|
||||
<div @click="tab = 'overview'" class="tab" :class="{active:tab === 'overview'}">
|
||||
<div class="ui top">
|
||||
<aside></aside>
|
||||
<nav>
|
||||
<div @click="tab = 'overview'" :class="{active:tab === 'overview'}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
|
||||
Overview
|
||||
</div>
|
||||
<div @click="tab = 'action'" class="tab" :class="{active:tab === 'action', disabled:!user}">
|
||||
<div @click="generated.content ? tab = 'action' : null" :class="{active:tab === 'action', disabled:!generated.content}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
|
||||
Action
|
||||
Action code
|
||||
</div>
|
||||
<div @click="tab = 'markdown'" class="tab" :class="{active:tab === 'markdown', disabled:!user}">
|
||||
<div @click="generated.content ? tab = 'markdown' : null" :class="{active:tab === 'markdown', disabled:!generated.content}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"></path></svg>
|
||||
Markdown
|
||||
Markdown code
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="container">
|
||||
<!-- Left section -->
|
||||
<div class="left">
|
||||
<div class="ui">
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="avatar">
|
||||
<div :style="{backgroundImage:`url(${avatar})`}"></div>
|
||||
<aside>
|
||||
|
||||
<div class="ui-avatar" :style="{backgroundImage:avatar ? `url(${avatar})` : 'none'}"></div>
|
||||
|
||||
<input type="text" v-model="user" placeholder="Your GitHub username" @keyup="mock">
|
||||
<button @click="generate" :disabled="(!user)||(generated.pending)">
|
||||
{{ generated.pending ? 'Working on it :)' : 'Generate your metrics!' }}
|
||||
</button>
|
||||
<small>{{ requests.limit }} GitHub requests remaining</small>
|
||||
|
||||
<div class="configuration">
|
||||
<b>🖼️ Template</b>
|
||||
<label v-for="template in templates.list" :key="template" v-show="templates.descriptions[template.name] !== '(hidden)'" :class="{'not-available':!template.enabled}" :title="!template.enabled ? 'This template is not enabled on this web instance, use GitHub actions instead!' : ''">
|
||||
<input type="radio" v-model="templates.selected" :value="template.name" @change="mock" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template.name] || template.name }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- User -->
|
||||
<div class="user step">
|
||||
<input type="text" name="user" v-model="user" maxlength="39" placeholder="Your GitHub username" :disabled="generated.pending">
|
||||
<button @click="generate" :disabled="(!user)||(generated.pending)">{{ generated.pending ? "Working on it :)" : "Generate your metrics !" }}</button>
|
||||
<!-- GitHub requests tracker -->
|
||||
<div class="gh-requests">{{ requests.remaining }} GitHub request{{ requests.remaining > 1 ? "s" : "" }} remaining</div>
|
||||
<div class="configuration" v-if="plugins.base.length">
|
||||
<b>🗃️ Base content</b>
|
||||
<label v-for="part in plugins.base" :key="part">
|
||||
<input type="checkbox" v-model="plugins.enabled.base[part]" @change="mock" :disabled="generated.pending">
|
||||
<span>{{ plugins.descriptions[`base.${part}`] || `base.${part}` }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Scrollable part -->
|
||||
<div class="scrollable">
|
||||
<div class="configuration" v-if="plugins.list.length">
|
||||
<b>🧩 Additional plugins</b>
|
||||
<label v-for="plugin in plugins.list" :key="plugin" :class="{'not-available':!plugin.enabled}" :title="!plugin.enabled ? 'This plugin is not enabled on web instance, use it with GitHub actions !' : ''">
|
||||
<input type="checkbox" v-model="plugins.enabled[plugin.name]" @change="mock" :disabled="(!plugin.enabled)||(generated.pending)">
|
||||
{{ plugins.descriptions[plugin.name] || plugin.name }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Template -->
|
||||
<div class="step">
|
||||
<h2>🖼️ Template</h2>
|
||||
<div class="templates">
|
||||
<label v-for="template in templates.list" :key="template" v-show="templates.descriptions[template.name] !== '(hidden)'" :class="{'not-available':!template.enabled}" :title="!template.enabled ? 'This template is not enabled on web instance, use it with GitHub actions !' : ''">
|
||||
<input type="radio" v-model="templates.selected" :value="template.name" @change="load" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template.name] || template.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="configuration" v-if="configure">
|
||||
<b>🔧 Configure plugins</b>
|
||||
|
||||
<template v-for="(input, key) in configure">
|
||||
<b v-if="typeof input === 'string'">{{ input }}</b>
|
||||
<label v-else class="option">
|
||||
<i>{{ input.text }}</i>
|
||||
<input type="checkbox" v-if="input.type === 'boolean'" v-model="plugins.options[key]" @change="mock">
|
||||
<input type="number" v-else-if="input.type === 'number'" v-model="plugins.options[key]" @change="mock" :min="input.min" :max="input.max">
|
||||
<select v-else-if="input.type === 'select'" v-model="plugins.options[key]" @change="mock">
|
||||
<option v-for="value in input.values" :value="value">{{ value }}</option>
|
||||
</select>
|
||||
<input type="text" v-else v-model="plugins.options[key]" @change="mock" :placeholder="input.placeholder">
|
||||
</label>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<div class="preview">
|
||||
|
||||
<div class="readme">
|
||||
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg>
|
||||
<span>{{ user }}</span><span class="slash">/</span>README<span class="md">.md</span>
|
||||
</div>
|
||||
|
||||
<div v-if="tab == 'overview'">
|
||||
<div class="error" v-if="generated.error">An error occurred while generating your metrics :( Please try again later.</div>
|
||||
<div class="image" :class="{pending:generated.pending}" v-html="generated.content||templates.placeholder.image"></div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'markdown'">
|
||||
Add the markdown below to your <i>README.md</i> at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="markdown" :code="embed"></Prism>
|
||||
</div>
|
||||
|
||||
<!-- Palette -->
|
||||
<div class="step">
|
||||
<h2>🌎 Color palette</h2>
|
||||
<div class="templates">
|
||||
<label>
|
||||
<input type="radio" v-model="palette" value="light">
|
||||
☀️ Light
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" v-model="palette" value="dark">
|
||||
🌕 Dark
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Base content -->
|
||||
<div class="step" v-if="plugins.base.length">
|
||||
<h2>🗃️ Base content</h2>
|
||||
<div class="plugins">
|
||||
<label v-for="part in plugins.base" :key="part">
|
||||
<input type="checkbox" v-model="plugins.enabled.base[part]" @change="load" :disabled="generated.pending">
|
||||
<span v-html="plugins.descriptions[`base.${part}`] || `base.${part}`"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins -->
|
||||
<div class="step" v-if="plugins.list.length">
|
||||
<h2>🧩 Additional plugins</h2>
|
||||
<div class="plugins">
|
||||
<label v-for="plugin in plugins.list" :key="plugin" :class="{'not-available':!plugin.enabled}" :title="!plugin.enabled ? 'This plugin is not enabled on web instance, use it with GitHub actions !' : ''">
|
||||
<input type="checkbox" v-model="plugins.enabled[plugin.name]" @change="load" :disabled="(!plugin.enabled)||(generated.pending)">
|
||||
{{ plugins.descriptions[plugin.name] || plugin.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugins options -->
|
||||
<div class="step" v-if="(plugins.enabled.tweets)||(plugins.enabled.music)||(plugins.enabled.pagespeed)||(plugins.enabled.languages)||(plugins.enabled.habits)||(plugins.enabled.posts)||(plugins.enabled.isocalendar)||(plugins.enabled.projects)||(plugins.enabled.topics)||(plugins.enabled.activity)">
|
||||
<h2>🔧 Configure plugins</h2>
|
||||
<div class="options">
|
||||
<div class="options-group" v-if="plugins.enabled.tweets">
|
||||
<h4>{{ plugins.descriptions.tweets }}</h4>
|
||||
<label>
|
||||
Number of tweets to display
|
||||
<input type="number" v-model="plugins.options['tweets.limit']" min="1" max="10" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.music">
|
||||
<h4>{{ plugins.descriptions.music }}</h4>
|
||||
<label>
|
||||
Playlist embed link
|
||||
<input type="text" v-model="plugins.options['music.playlist']" placeholder="https://embed.music.apple.com/en/playlist/" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Number of tracks to display
|
||||
<input type="number" v-model="plugins.options['music.limit']" min="1" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.pagespeed">
|
||||
<h4>{{ plugins.descriptions.pagespeed }}</h4>
|
||||
<label>
|
||||
Detailed PageSpeed report
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Include a website screenshot
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.languages">
|
||||
<h4>{{ plugins.descriptions.languages }}</h4>
|
||||
<label>
|
||||
Ignored languages (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.ignored']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Skipped repositories (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.skipped']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.habits">
|
||||
<h4>{{ plugins.descriptions.habits }}</h4>
|
||||
<label>
|
||||
Number of events for habits
|
||||
<input type="number" v-model="plugins.options['habits.from']" min="1" max="1000" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Number of days for habits
|
||||
<input type="number" v-model="plugins.options['habits.days']" min="1" max="30" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Display tidbits
|
||||
<input type="checkbox" v-model="plugins.options['habits.facts']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Display activity charts
|
||||
<input type="checkbox" v-model="plugins.options['habits.charts']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.posts">
|
||||
<h4>{{ plugins.descriptions.posts }}</h4>
|
||||
<label>
|
||||
Posts source
|
||||
<select v-model="plugins.options['posts.source']" disabled>
|
||||
<option value="dev.to">dev.to</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Number of posts to display
|
||||
<input type="number" v-model="plugins.options['posts.limit']" min="1" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.isocalendar">
|
||||
<h4>{{ plugins.descriptions.isocalendar }}</h4>
|
||||
<label>
|
||||
Isocalendar duration
|
||||
<select v-model="plugins.options['isocalendar.duration']" :disabled="generated.pending">
|
||||
<option value="half-year">Half year</option>
|
||||
<option value="full-year">Full year</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.topics">
|
||||
<h4>{{ plugins.descriptions.topics }}</h4>
|
||||
<label>
|
||||
Topics display mode
|
||||
<select v-model="plugins.options['topics.mode']" @change="load" :disabled="generated.pending">
|
||||
<option value="starred">Starred topics</option>
|
||||
<option value="mastered">Known and mastered technologies</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Topics sorting
|
||||
<select v-model="plugins.options['topics.sort']" :disabled="generated.pending">
|
||||
<option value="starred">Recently starred by you</option>
|
||||
<option value="stars">Most stars</option>
|
||||
<option value="activity">Recent actity</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Number of topics to display
|
||||
<input type="number" v-model="plugins.options['topics.limit']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.projects">
|
||||
<h4>{{ plugins.descriptions.projects }}</h4>
|
||||
<label>
|
||||
Number of projects to display
|
||||
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
<label>
|
||||
Repositories projects to display (comma separated)
|
||||
<input type="text" v-model="plugins.options['projects.repositories']" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.activity">
|
||||
<h4>{{ plugins.descriptions.activity }}</h4>
|
||||
<label>
|
||||
Number of activity events to display
|
||||
<input type="number" v-model="plugins.options['activity.limit']" min="1" max="10" @change="load" :disabled="generated.pending">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab == 'action'">
|
||||
Create a new workflow with the following content at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="yaml" :code="action"></Prism>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right section -->
|
||||
<div class="right">
|
||||
</div>
|
||||
|
||||
<!-- Tabs (mobile screen)-->
|
||||
<nav class="mobile flex">
|
||||
<div class="left"></div>
|
||||
<div class="right">
|
||||
<div @click="tab = 'overview'" class="tab" :class="{active:tab === 'overview'}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 1.75A.75.75 0 01.75 1h4.253c1.227 0 2.317.59 3 1.501A3.744 3.744 0 0111.006 1h4.245a.75.75 0 01.75.75v10.5a.75.75 0 01-.75.75h-4.507a2.25 2.25 0 00-1.591.659l-.622.621a.75.75 0 01-1.06 0l-.622-.621A2.25 2.25 0 005.258 13H.75a.75.75 0 01-.75-.75V1.75zm8.755 3a2.25 2.25 0 012.25-2.25H14.5v9h-3.757c-.71 0-1.4.201-1.992.572l.004-7.322zm-1.504 7.324l.004-5.073-.002-2.253A2.25 2.25 0 005.003 2.5H1.5v9h3.757a3.75 3.75 0 011.994.574z"></path></svg>
|
||||
Overview
|
||||
</div>
|
||||
<div @click="tab = 'action'" class="tab" :class="{active:tab === 'action', disabled:!user}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
|
||||
Action
|
||||
</div>
|
||||
<div @click="tab = 'markdown'" class="tab" :class="{active:tab === 'markdown', disabled:!user}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"></path></svg>
|
||||
Markdown
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="body">
|
||||
<div class="readme">
|
||||
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg>
|
||||
<span>{{ user }}</span><span class="slash">/</span>README<span class="md">.md</span>
|
||||
</div>
|
||||
<!-- Overview -->
|
||||
<section class="preview" v-if="tab == 'overview'">
|
||||
Once generated, click on tabs above to see the code to embed them on your real readme !<br>
|
||||
<template v-if="generated.content">
|
||||
<img class="metrics" :src="generated.content" alt="metrics">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img class="metrics" :src="templates.placeholder" alt="metrics">
|
||||
</template>
|
||||
<div class="error" v-if="generated.error">An error occurred. Please try again later.</div>
|
||||
</section>
|
||||
<!-- Markdown -->
|
||||
<section v-else-if="tab == 'markdown'">
|
||||
Add the markdown below in your <i>README.md</i> at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="markdown" :code="embed"></Prism>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Action -->
|
||||
<section v-else-if="tab == 'action'">
|
||||
Create a new workflow with the following content at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code">
|
||||
<Prism language="yaml" :code="action"></Prism>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
<!-- Scripts -->
|
||||
@@ -307,8 +130,10 @@
|
||||
<script src="/.js/prism.markdown.min.js"></script>
|
||||
<script src="/.js/prism.yaml.min.js"></script>
|
||||
<script src="/.js/ejs.min.js"></script>
|
||||
<script src="/.js/faker.min.js"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/vue.prism.min.js"></script>
|
||||
<script src="/.js/app.placeholder.js"></script>
|
||||
<script src="/.js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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":{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<% for (const partial of [...partials]) { %>
|
||||
<%- await include(`partials/${partial}.ejs`); %>
|
||||
<%- await include(`partials/${partial}.ejs`) %>
|
||||
<% } %>
|
||||
|
||||
<% if (base.metadata) { %>
|
||||
|
||||
|
Before Width: | Height: | Size: 978 B After Width: | Height: | Size: 977 B |
@@ -3,7 +3,7 @@
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
|
||||
|
||||
People
|
||||
</h2>
|
||||
<div class="row">
|
||||
<section>
|
||||
@@ -29,7 +29,7 @@
|
||||
<%= plugins.people.error.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<% for (const user of plugins.people.followers) { %><img class="avatar" src="data:image/png;base64,<%= user.avatar %>" width="<%= plugins.people.size %>" height="<%= plugins.people.size %>" alt=""/><% } %>
|
||||
<% for (const user of plugins.people.followers) { %><img class="avatar" src="data:image/png;base64,<%= user.avatar %>" width="<%= plugins.people.size %>" height="<%= plugins.people.size %>" alt="" /><% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
<%= plugins.people.error.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<% for (const user of plugins.people.following) { %><img class="avatar" src="data:image/png;base64,<%= user.avatar %>" width="<%= plugins.people.size %>" height="<%= plugins.people.size %>" alt=""/><% } %>
|
||||
<% for (const user of plugins.people.following) { %><img class="avatar" src="data:image/png;base64,<%= user.avatar %>" width="<%= plugins.people.size %>" height="<%= plugins.people.size %>" alt="" /><% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</section>
|
||||
<% } else { %>
|
||||
<% for (const partial of [...partials]) { %>
|
||||
<%- await include(`partials/${partial}.ejs`); %>
|
||||
<%- await include(`partials/${partial}.ejs`) %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -50,7 +50,7 @@ WARRANTY, to the extent permitted by applicable law.
|
||||
|
||||
Last generated: <%= new Date().toGMTString() %>
|
||||
</div><% } -%>
|
||||
<% for (const partial of [...partials]) { %><%- await include(`partials/${partial}.ejs`); %><% } -%>
|
||||
<% for (const partial of [...partials]) { %><%- await include(`partials/${partial}.ejs`) %><% } -%>
|
||||
<% if (base.metadata) { -%>
|
||||
|
||||
<footer>Connection reset by <%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %></footer><%# -%>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,7 +1,7 @@
|
||||
<% if (base.header) { %>
|
||||
<div class="stdin"><%- meta.$ %> whoami</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
<b><%= user.name || user.login %></b> registered=<%= computed.registration.match(/^.+? [ym]/)[0].replace(/ /g, "") %>, uid=<%= `${user.databaseId}`.substr(-4) %>, gid=<%= user.organizations.totalCount %>
|
||||
<b><%= user.name || user.login %></b> 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") %> <b><% for (const [x, {color}] of Object.entries(computed.calendar)) { -%><span style="color:<%= color %>">#</span><% } %></b>
|
||||
followed by <b><%= user.followers.totalCount %></b> user<%= s(user.followers.totalCount) %>
|
||||
</div><% } -%>
|
||||
Reference in New Issue
Block a user