Full plugins and server redesign

- Languages, issues and pr are now plugins (but enabled by default for retro-compatibility)
- Query parameters are now parsed correctly
- Redesigned server index with vue.js
This commit is contained in:
lowlighter
2020-10-20 21:56:05 +02:00
parent 69e63541dc
commit 9bd7da1740
20 changed files with 718 additions and 394 deletions

BIN
.github/readme/imgs/plugin_followup.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
.github/readme/imgs/plugin_languages.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -501,6 +501,74 @@ Add the following to your `settings.json` and pass `?habits=1` in url when gener
</details>
#### ✒️ Follow-up
The *follow-up* plugin allows you to compute the ratio of opened/closed issues and the ratio of opened/merged pull requests on your repositories, which shows whether most of them are maintened or not.
![Follow-up plugin](https://github.com/lowlighter/metrics/blob/master/.github/readme/imgs/plugin_followup.png)
<details>
<summary>💬 About</summary>
This plugin is enabled by default. To disable it, explicitly opt-out.
##### Setup with GitHub actions
Add the following to your workflow :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_followup: yes
```
##### Setup in your own instance
Add the following to your `settings.json` and pass `?followup=1` in url when generating metrics.
```json
"plugins":{
"followup":{
"enabled":true,
}
}
```
</details>
#### 🈷️ Languages
The *languages* plugin allows you to compute which languages you use the most in your repositories.
![Languages plugin](https://github.com/lowlighter/metrics/blob/master/.github/readme/imgs/plugin_languages.png)
<details>
<summary>💬 About</summary>
This plugin is enabled by default. To disable it, explicitly opt-out.
##### Setup with GitHub actions
Add the following to your workflow :
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
plugin_languages: yes
```
##### Setup in your own instance
Add the following to your `settings.json` and pass `?languages=1` in url when generating metrics.
```json
"plugins":{
"languages":{
"enabled":true,
}
}
```
</details>
#### ⏭️ Selfskip
The *selfskip* plugin allows you to count out all commits tagged with `[Skip GitHub Action]` you authored on your personal repository from your reported commit counts.
@@ -612,6 +680,8 @@ This way you'll be able to rapidly test SVG renders with your browser.
* To make HTTP/S requests
* [actions/toolkit](https://github.com/actions/toolkit/tree/master) and [vercel/ncc](https://github.com/vercel/ncc)
* To build the GitHub Action
* [vuejs/vue](https://github.com/vuejs/vue)
* To display server application
All icons were ripped across GitHub's site, but still remains the intellectual property of GitHub.
See [GitHub Logos and Usage](https://github.com/logos) for more information.

View File

@@ -41,6 +41,12 @@ inputs:
plugin_selfskip:
description: Skip commits flagged with [Skip GitHub Action] from commits count
default: no
plugin_languages:
description: Enable most used languages metrics
default: yes
plugin_followup:
description: Enable owned repositories issues and pull requests metrics
default: yes
debug:
description: Enable debug logs
default: no

File diff suppressed because one or more lines are too long

5
package-lock.json generated
View File

@@ -1566,6 +1566,11 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"vue": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
"wide-align": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",

View File

@@ -7,7 +7,7 @@
"start": "node index.mjs",
"build": "node utils/build.mjs",
"test": "node tests/metrics.mjs",
"upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest svgo@latest @vercel/ncc@latest libxmljs@latest"
"upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest svgo@latest vue@latest @vercel/ncc@latest libxmljs@latest"
},
"repository": {
"type": "git",
@@ -31,7 +31,8 @@
"express-rate-limit": "^5.1.3",
"image-to-base64": "^2.1.1",
"memory-cache": "^0.2.0",
"svgo": "^1.3.2"
"svgo": "^1.3.2",
"vue": "^2.6.12"
},
"devDependencies": {
"@vercel/ncc": "^0.24.1",

View File

@@ -27,6 +27,12 @@
"habits":{ "//":"Habits plugin",
"enabled":true, "//":"Enable or disable coding habits metrics",
"from":50, "//":"Number of activity events to base habits on (up to 100)"
},
"languages":{ "//":"Languages plugins",
"enabled":true, "//":"Enable or disable most used languages metrics (*this plugin is enabled by default)"
},
"followup":{ "//":"Follow-up plugin",
"enabled":true, "//":"Enable owned repositories issues and pull requests metrics (*this plugin is enabled by default)"
}
}
}

View File

@@ -7,6 +7,7 @@
import compression from "compression"
import setup from "./setup.mjs"
import metrics from "./metrics.mjs"
import Templates from "./templates/index.mjs"
/** App */
export default async function () {
@@ -40,10 +41,24 @@
//Base routes
const limiter = ratelimit({max:60, windowMs:60*1000})
const templates = [...new Set([conf.settings.templates.default, ...(conf.settings.templates.enabled.length ? Object.keys(Templates).filter(key => conf.settings.templates.enabled.includes(key)) : Object.keys(Templates))])]
const enabled = Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)
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("/placeholder.svg", limiter, (req, res) => res.sendFile(`${conf.statics}/placeholder.svg`))
app.get("/favicon.ico", limiter, (req, res) => res.sendStatus(204))
app.get("/plugins.list", limiter, (req, res) => res.status(200).json(enabled))
app.get("/templates.list", limiter, (req, res) => res.status(200).json(templates))
app.get("/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/ejs/ejs.min.js`))
app.get("/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.js`))
app.get("/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.map`))
app.get("/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue/dist/vue.min.js`))
app.get("/placeholder.svg", limiter, async (req, res) => {
const template = req.query.template || conf.settings.templates.default
if (!(template in Templates))
return res.sendStatus(404)
const {style, placeholder} = conf.templates[template]
res.status(200).json({style, placeholder})
})
//Metrics
app.get("/:login", ...middlewares, async (req, res) => {
@@ -69,7 +84,7 @@
//Compute rendering
try {
//Render
const rendered = await metrics({login, q:req.query}, {graphql, rest, plugins, conf})
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf})
//Cache
if ((!debug)&&(cached))
cache.put(login, rendered, cached)
@@ -103,6 +118,22 @@
`Cached time | ${cached} seconds`,
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled | ${Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).join(", ")}`
`Plugins enabled | ${enabled.join(", ")}`
].join("\n")))
}
/** Query parser */
function parse(query) {
for (const [key, value] of Object.entries(query)) {
//Parse number
if (/^\d+$/.test(value))
query[key] = Number(value)
//Parse boolean
if (/^true|false$/.test(value))
query[key] = !!value
//Parse null
if (/^null$/.test(value))
query[key] = null
}
return query
}

View File

@@ -9,93 +9,172 @@
</head>
<body>
<h1><a href="https://github.com/lowlighter/metrics">GitHub metrics</a></h1>
<main :class="[palette]">
<p>
Enter your GitHub username below to generate your metrics.
</p>
<h1><a href="https://github.com/lowlighter/metrics">Generate your metrics !</a></h1>
<label>
<input type="text" name="user" placeholder="@username" value="">
</label>
<div id="metrics">
<img class="placeholder" src="/placeholder.svg">
<img class="generated" src="/placeholder.svg">
</div>
<aside>
Embed these metrics on your GitHub profile by adding the markdown below in your <i>README.md</i> at <a id="user-repo" href="#"><span class="user"></span>/<span class="user"></span></a>
<br>
<div class="code">
![<span class="md-alt">GitHub metrics</span>](<span class="url">https://metrics.lecoq.io/my-github-user</span>)
<div class="step">
<h2>1. Enter your GitHub username</h2>
<label>
<input type="text" v-model="user" maxlength="39" placeholder="GitHub username" :disabled="generated.pending">
</label>
</div>
For even more metrics (coding habits, PageSpeed performances, number of line of code you wrote, page views, etc.), setup this as a <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a> on your repository !<br>
Check out <a href="https://github.com/lowlighter/metrics">lowlighter/metrics</a> for more informations
</aside>
<div class="step">
<h2>2. Select a template and enable additional plugins</h2>
<div class="templates">
<label v-for="template in templates.list" :key="template">
<input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending">
{{ templates.descriptions[template] || template }}
</label>
</div>
<div class="plugins">
<label v-for="plugin in plugins.list" :key="plugin">
<input type="checkbox" v-model="plugins.enabled[plugin]" @change="load" :disabled="generated.pending">
{{ plugins.descriptions[plugin] || plugin }}
</label>
</div>
<div class="cache-notice">
*To reduce server overhead, metrics are cached. Changes may not be reflected until cache expiration.
</div>
<div class="palette">
Generated metrics use transparency and colors which matches both light and dark modes
<div>
<label>
<input type="radio" v-model="palette" value="light">
☀️ Light mode
</label>
<label>
<input type="radio" v-model="palette" value="dark">
🌙 Night mode
</label>
</div>
</div>
</div>
<div class="step">
<h2>3. Generate your metrics</h2>
<template v-if="generated.content">
<img :src="generated.content" alt="metrics">
</template>
<template v-else>
<img :src="templates.placeholder" alt="metrics">
<button @click="generate" :disabled="(!user)||(generated.pending)">{{ generated.pending ? "Generating your metrics..." : user ? "Generate your metrics" : "Enter your GitHub username first" }}</button>
</template>
</div>
<div class="step">
<h2>4. Embed these metrics on your GitHub profile</h2>
<template v-if="user">
Add the markdown below in your <i>README.md</i> at <a :href="repo">{{ user }}/{{ user }}</a>
<div class="code">
![<span class="md-alt">GitHub metrics</span>]({{ url }})
</div>
</template>
For even more features, setup <a href="https://github.com/lowlighter/metrics">lowlighter/metrics</a> as a <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a> !
</div>
</main>
<script src="/axios.min.js"></script>
<script src="/ejs.min.js"></script>
<script src="/vue.min.js"></script>
<script>
window.onload = function () {
//User updater
let timeout = null
document.querySelector("input[name=user]").onkeyup = function (event) {
//Retrieve user value
clearTimeout(timeout)
const user = event.target.value
//Display placeholder
document.querySelector("#metrics .placeholder").style.opacity = 1
document.querySelector("#metrics .generated").style.opacity = 0
document.querySelector("aside").style.opacity = 0
//Update github user
document.querySelector(".code .url").innerText = `${window.location.href}${user}`
document.querySelector("#user-repo").href = `https://github.com/${user}/${user}`
document.querySelectorAll(".user").forEach(node => node.innerText = user)
//Update metrics
if (event.key === "Enter")
metrics(user)
else
timeout = setTimeout(() => metrics(user), 1800)
}
//Metrics updater
let current = null
function metrics(user) {
if (!user.trim().length)
return
if (current === user) {
document.querySelector("#metrics .placeholder").style.opacity = 0
document.querySelector("#metrics .generated").style.opacity = 1
document.querySelector("aside").style.opacity = 1
}
else {
current = user
document.querySelector("#metrics .generated").src = `${window.location.href}${user}`
document.querySelector("#metrics .generated").onload = function () {
document.querySelector("#metrics .placeholder").style.opacity = 0
document.querySelector("#metrics .generated").style.opacity = 1
document.querySelector("aside").style.opacity = 1
;(async function() {
new Vue({
el:"main",
async mounted() {
await this.load()
},
data:{
user:"",
palette:"light",
plugins:{
list:(await axios.get("/plugins.list")).data,
enabled:{languages:true, followup:true},
descriptions:{
pagespeed:"Website performances",
languages:"Most used languages",
followup:"Owned repositories issues and pull requests",
traffic:"Pages views",
lines:"Lines of code changed",
habits:"Coding habits",
selfskip:"Skip metrics commits",
},
},
templates:{
list:(await axios.get("/templates.list")).data,
loaded:{},
selected:(await axios.get("/templates.list")).data[0],
placeholder:"",
descriptions:{
classic:"Classic template",
},
},
generated:{
pending:false,
content:"",
},
},
computed:{
repo() {
return `https://github.com/${this.user}/${this.user}`
},
url() {
const plugins = Object.entries(this.plugins.enabled)
.filter(([key, value]) => /^languages|followup$/.test(key) ? !value : value)
.map(([key, value]) => `${key}=${+value}`)
.join("&")
return `${window.location.href}${this.user}${plugins.length ? `?${plugins}` : ""}`
},
},
methods:{
async load() {
const template = this.templates.selected
if (!this.templates.loaded[template]) {
const {data:{placeholder, style}} = await axios.get(`/placeholder.svg?template=${template}`)
this.templates.loaded[template] = {placeholder, style}
}
}
}
}
const {placeholder = "", style = {}} = this.templates.loaded[this.templates.selected] || {}
this.templates.placeholder = placeholder ? this.serialize(ejs.render(placeholder, {plugins:this.plugins.enabled, style})) : "#"
},
async generate() {
this.generated.pending = true
this.generated.content = this.serialize((await axios.get(this.url)).data)
},
serialize(svg) {
return `data:image/svg+xml;base64,${btoa(svg)}`
},
},
})
})()
</script>
<style>
body {
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
}
main {
background-color: #FFFFFF;
color: #1B1F23;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100vw;
padding: 0;
margin: 0;
overflow-x: hidden;
padding-bottom: 2rem;
width: 100%;
transition: background-color .3s;
}
h1 {
margin: 4rem 0 0;
font-size: 1.6rem;
margin: 1rem 0;
}
h2 {
margin: 1.5rem 0 1rem;
font-size: 1.3rem;
}
a, a:hover, a:visited {
color: #0366D6;
@@ -108,39 +187,47 @@
transition: color .4s;
cursor: pointer;
}
input {
input, button, select {
border-radius: .5rem;
padding: .5rem 1rem;
padding: .25rem .5rem;
outline: none;
border: 1px solid #E1E4E8;
background-color: #FAFBFC;
color: #1B1F23;
font-size: 1.2rem;
text-align: center;
margin: 0 0 1.5rem;
cursor: pointer;
}
input:focus {
outline: none;
}
#metrics {
position: relative;
max-width: 100%;
width: 480px;
height: 485px;
input[type=text], select, button {
min-width: 50%;
font-size: 1.1rem;
}
#metrics img {
position: absolute;
top: 0;
left: 0;
width: 100%;
transition: opacity .4s;
}
aside {
opacity: 0;
padding: .25rem;
transition: opacity .4s;
option {
text-align: center;
margin: 1rem 0 2rem;
}
label, button {
margin: 1rem;
}
input[disabled], button[disabled], select[disabled] {
opacity: .5;
cursor: not-allowed;
}
.step {
margin-bottom: 1rem;
text-align: center;
max-width: 800px;
}
.plugins {
margin-top: 1rem;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.plugins label {
margin: 0 1rem;
}
.code {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
@@ -152,6 +239,29 @@
.code .md-alt {
color: #6F42C1;
}
.cache-notice {
margin-top: .5rem;
font-size: .9rem;
opacity: .8;
}
.palette {
margin-top: 1rem;
}
main.dark {
background-color: #181A1B;
color: #D4D1C5;
}
.dark a, .dark a:visited {
color: #4CACEE;
}
.dark input, .dark button {
color: #D4D1C5;
background-color: #1A1C1E;
border-color: #373C3E;
}
.dark .code {
background-color: #1A1C1E;
}
</style>
</body>

View File

@@ -11,6 +11,8 @@
try {
//Init
console.debug(`metrics/compute/${login} > start`)
console.debug(JSON.stringify(q))
const template = q.template || conf.settings.templates.default
const pending = []
const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end

View File

@@ -0,0 +1,41 @@
//Setup
export default function ({login, data, computed, pending, q}, {enabled = true} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.followup = null
if (("followup" in q)&&(!q.followup))
return computed.plugins.followup = null
console.debug(`metrics/compute/${login}/plugins > followup`)
computed.svg.height += 70
//Plugin execution
pending.push(new Promise(async solve => {
try {
//Define getters
const followup = {
issues:{
get count() { return this.open + this.closed },
get open() { return computed.repositories.issues_open },
get closed() { return computed.repositories.issues_closed },
},
pr:{
get count() { return this.open + this.merged },
get open() { return computed.repositories.pr_open },
get merged() { return computed.repositories.pr_merged }
}
}
//Save results
computed.plugins.followup = followup
console.debug(`metrics/compute/${login}/plugins > followup > success`)
console.debug(JSON.stringify(computed.plugins.followup))
solve()
}
catch (error) {
//Generic error
computed.plugins.followup = {error:`An error occured`}
console.debug(`metrics/compute/${login}/plugins > followup > error`)
console.debug(error)
solve()
}
}))
}

View File

@@ -1,5 +1,7 @@
//Imports
import followup from "./followup/index.mjs"
import habits from "./habits/index.mjs"
import languages from "./languages/index.mjs"
import lines from "./lines/index.mjs"
import pagespeed from "./pagespeed/index.mjs"
import selfskip from "./selfskip/index.mjs"
@@ -7,7 +9,9 @@
//Exports
export default {
followup,
habits,
languages,
lines,
pagespeed,
selfskip,

View File

@@ -0,0 +1,42 @@
//Setup
export default function ({login, data, computed, pending, q}, {enabled = true} = {}) {
//Check if plugin is enabled and requirements are met
if (!enabled)
return computed.plugins.languages = null
if (("languages" in q)&&(!q.languages))
return computed.plugins.languages = null
console.debug(`metrics/compute/${login}/plugins > languages`)
computed.svg.height += 90
//Plugin execution
pending.push(new Promise(async solve => {
try {
//Iterate through user's repositories and retrieve languages data
const languages = {colors:{}, total:0, stats:{}}
for (const repository of data.user.repositories.nodes) {
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
languages.stats[name] = (languages.stats[name] || 0) + size
languages.colors[name] = color || "#ededed"
languages.total += size
}
}
//Compute languages stats
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
for (let i = 1; i < languages.favorites.length; i++)
languages.favorites[i].x = languages.favorites[i-1].x + languages.favorites[i-1].value
//Save results
computed.plugins.languages = languages
console.debug(`metrics/compute/${login}/plugins > languages > success`)
console.debug(JSON.stringify(computed.plugins.languages))
solve()
}
catch (error) {
//Generic error
computed.plugins.languages = {error:`An error occured`}
console.debug(`metrics/compute/${login}/plugins > languages > error`)
console.debug(error)
solve()
}
}))
}

View File

@@ -20,6 +20,7 @@
}
//Save results
computed.plugins.selfskip = {commits}
computed.commits -= commits
console.debug(`metrics/compute/${login}/plugins > selfskip > success`)
console.debug(JSON.stringify(computed.plugins.selfskip))
solve()

View File

@@ -14,7 +14,7 @@
if (!q.traffic)
return computed.plugins.traffic = null
console.debug(`metrics/compute/${login}/plugins > traffic`)
computed.svg.height += 20
computed.svg.height += !q.lines ? 20 : 0
//Plugin execution
pending.push(new Promise(async solve => {

View File

@@ -11,7 +11,8 @@
const conf = {
templates:{},
settings:{},
statics:path.resolve("src/html")
statics:path.resolve("src/html"),
node_modules:path.resolve("node_modules"),
}
//Load settings
@@ -37,19 +38,20 @@
const files = [
`${templates}/${name}/query.graphql`,
`${templates}/${name}/image.svg`,
`${templates}/${name}/placeholder.svg`,
`${templates}/${name}/style.css`,
]
const [query, image, style] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
conf.templates[name] = {query, image, style}
const [query, image, placeholder, style] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
conf.templates[name] = {query, image, placeholder, style}
console.debug(`metrics/setup > load template [${name}] > success`)
//Debug
if (conf.settings.debug) {
Object.defineProperty(conf.templates, name, {
get() {
console.debug(`metrics/setup > reload template [${name}]`)
const [query, image, style] = files.map(file => `${fs.readFileSync(path.resolve(file))}`)
const [query, image, placeholder, style] = files.map(file => `${fs.readFileSync(path.resolve(file))}`)
console.debug(`metrics/setup > reload template [${name}] > success`)
return {query, image, style}
return {query, image, placeholder, style}
}
})
}

View File

@@ -54,7 +54,7 @@
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M10.5 7.75a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0zm1.43.75a4.002 4.002 0 01-7.86 0H.75a.75.75 0 110-1.5h3.32a4.001 4.001 0 017.86 0h3.32a.75.75 0 110 1.5h-3.32z"></path></svg>
<%= computed.commits - (computed.plugins.selfskip ? computed.plugins.selfskip.commits||0 : 0) %> Commit<%= s(computed.commits - (computed.plugins.selfskip ? computed.plugins.selfskip.commits||0 : 0)) %>
<%= computed.commits %> Commit<%= s(computed.commits) %>
</div>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 1.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v7.736a.75.75 0 101.5 0V1.75A1.75 1.75 0 0011.25 0h-8.5A1.75 1.75 0 001 1.75v11.5c0 .966.784 1.75 1.75 1.75h3.17a.75.75 0 000-1.5H2.75a.25.25 0 01-.25-.25V1.75zM4.75 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM4 7.75A.75.75 0 014.75 7h2a.75.75 0 010 1.5h-2A.75.75 0 014 7.75zm11.774 3.537a.75.75 0 00-1.048-1.074L10.7 14.145 9.281 12.72a.75.75 0 00-1.062 1.058l1.943 1.95a.75.75 0 001.055.008l4.557-4.45z"></path></svg>
@@ -165,74 +165,105 @@
</div>
</section>
<div class="row">
<% if (computed.plugins.followup) { %>
<div class="row">
<section class="column">
<h3>Issues</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= computed.repositories.issues_count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (computed.repositories.issues_closed/computed.repositories.issues_count)*220 || 0 %>" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="<%= (computed.repositories.issues_closed/computed.repositories.issues_count)*220 || 0 %>" y="0" width="<%= (1-computed.repositories.issues_closed/computed.repositories.issues_count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
<span class="no-wrap"><%= computed.repositories.issues_closed %> Closed</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" 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 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
<span class="no-wrap"><%= computed.repositories.issues_open %> Open</span>
</div>
</div>
</section>
<section class="column">
<h3>Issues</h3>
<% if (computed.plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.47.22A.75.75 0 015 0h6a.75.75 0 01.53.22l4.25 4.25c.141.14.22.331.22.53v6a.75.75 0 01-.22.53l-4.25 4.25A.75.75 0 0111 16H5a.75.75 0 01-.53-.22L.22 11.53A.75.75 0 010 11V5a.75.75 0 01.22-.53L4.47.22zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5H5.31zM8 4a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 4zm0 8a1 1 0 100-2 1 1 0 000 2z"></path></svg>
<%= computed.plugins.followup.error %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= computed.plugins.followup.issues.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (computed.plugins.followup.issues.closed/computed.plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#d73a49"/>
<rect mask="url(#issues-bar)" x="<%= (computed.plugins.followup.issues.closed/computed.plugins.followup.issues.count)*220 || 0 %>" y="0" width="<%= (1-computed.plugins.followup.issues.closed/computed.plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"></path></svg>
<span class="no-wrap"><%= computed.plugins.followup.issues.closed %> Closed</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" 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 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
<span class="no-wrap"><%= computed.plugins.followup.issues.open %> Open</span>
</div>
</div>
<% } %>
</section>
<section class="column">
<h3>Pull requests</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= computed.repositories.pr_count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (computed.repositories.pr_merged/computed.repositories.pr_count)*220 || 0 %>" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="<%= (computed.repositories.pr_merged/computed.repositories.pr_count)*220 || 0 %>" y="0" width="<%= (1-computed.repositories.pr_merged/computed.repositories.pr_count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<span class="no-wrap"><%= computed.repositories.pr_merged %> Merged</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
<span class="no-wrap"><%= computed.repositories.pr_open %> Open</span>
</div>
</div>
</section>
<section class="column">
<h3>Pull requests</h3>
<% if (computed.plugins.followup.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.47.22A.75.75 0 015 0h6a.75.75 0 01.53.22l4.25 4.25c.141.14.22.331.22.53v6a.75.75 0 01-.22.53l-4.25 4.25A.75.75 0 0111 16H5a.75.75 0 01-.53-.22L.22 11.53A.75.75 0 010 11V5a.75.75 0 01.22-.53L4.47.22zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5H5.31zM8 4a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 4zm0 8a1 1 0 100-2 1 1 0 000 2z"></path></svg>
<%= computed.plugins.followup.error %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= computed.plugins.followup.pr.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (computed.plugins.followup.pr.merged/computed.plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#6f42c1"/>
<rect mask="url(#pr-bar)" x="<%= (computed.plugins.followup.pr.merged/computed.plugins.followup.pr.count)*220 || 0 %>" y="0" width="<%= (1-computed.plugins.followup.pr.merged/computed.plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#28a745"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<span class="no-wrap"><%= computed.plugins.followup.pr.merged %> Merged</span>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"></path></svg>
<span class="no-wrap"><%= computed.plugins.followup.pr.open %> Open</span>
</div>
</div>
<% } %>
</section>
</div>
<section class="column">
<h3>Most used languages</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="<%= computed.languages.favorites.length ? 0 : 460 %>" height="8" fill="#d1d5da"/>
<% for (const {name, value, color, x} of computed.languages.favorites) { %>
<rect mask="url(#languages-bar)" x="<%= x*460 %>" y="0" width="<%= value*460 %>" height="8" fill="<%= color %>"/>
<% } %>
</svg>
<div class="field center horizontal-wrap fill-width">
<% for (const {name, value, color} of computed.languages.favorites) { %>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="<%= color %>" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
<%= name %>
</div>
<% } %>
</div>
</section>
<% } %>
<% if (computed.plugins.languages) { %>
<section class="column">
<h3>Most used languages</h3>
<% if (computed.plugins.languages.error) { %>
<section>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.47.22A.75.75 0 015 0h6a.75.75 0 01.53.22l4.25 4.25c.141.14.22.331.22.53v6a.75.75 0 01-.22.53l-4.25 4.25A.75.75 0 0111 16H5a.75.75 0 01-.53-.22L.22 11.53A.75.75 0 010 11V5a.75.75 0 01.22-.53L4.47.22zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5H5.31zM8 4a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 018 4zm0 8a1 1 0 100-2 1 1 0 000 2z"></path></svg>
<%= computed.plugins.languages.error %>
</div>
</section>
<% } else { %>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="<%= computed.plugins.languages.favorites.length ? 0 : 460 %>" height="8" fill="#d1d5da"/>
<% for (const {name, value, color, x} of computed.plugins.languages.favorites) { %>
<rect mask="url(#languages-bar)" x="<%= x*460 %>" y="0" width="<%= value*460 %>" height="8" fill="<%= color %>"/>
<% } %>
</svg>
<div class="field center horizontal-wrap fill-width">
<% for (const {name, value, color} of computed.plugins.languages.favorites) { %>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="<%= color %>" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
<%= name %>
</div>
<% } %>
</div>
<% } %>
</section>
<% } %>
<% if (computed.plugins.pagespeed) { %>
<div class="row">

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,130 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="485">
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 355 + (!!plugins.followup)*70 + (!!plugins.habits)*70 + (!!plugins.languages)*90 + ((!!plugins.lines)+(!!plugins.traffic))*20 + (!!plugins.pagespeed)*130 %>">
<style>
/* SVG global context */
svg {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px;
color: #777777;
}
/* Headers */
h1, h2, h3 {
margin: 8px 0 2px;
padding: 0;
color: #0366d6;
font-weight: normal;
}
h1 svg, h2 svg, h3 svg {
fill: currentColor;
}
h1 {
font-size: 20px;
font-weight: bold;
}
h2 {
font-size: 16px;
}
h3 {
font-size: 14px;
}
/* Fields */
section > .field {
margin-left: 5px;
margin-right: 5px;
}
.field {
display: flex;
align-items: center;
margin-bottom: 2px;
}
.field svg {
margin: 0 8px;
fill: #959da5;
}
/* Displays */
.row {
display: flex;
}
.row section {
flex: 1 1 0;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
}
.center {
justify-content: center;
}
.horizontal {
justify-content: space-around;
}
.horizontal-wrap {
flex-wrap: wrap;
}
.horizontal .field {
flex: 1 1 0;
}
.no-wrap {
white-space: nowrap;
}
.fill-width {
width: 100%;
}
/* User avatar */
.avatar {
background-color: rgba(119,119,119,.62);
border-radius: 50%;
margin: 0 6px;
height: 20px;
width: 20px;
}
/* Commit calendar */
.calendar.field {
margin: 4px 0;
margin-left: 7px;
}
.calendar .day {
outline: 1px solid rgba(27,31,35,.04);
outline-offset: -1px;
}
/* Progress bars */
svg.bar {
margin: 4px 0;
}
/* Language */
.field.language {
margin: 0 8px;
flex-grow: 0;
}
/* Placeholder */
.placeholder {
background-color: rgba(119,119,119,.62);
margin: 2px 4px 0px;
height: 12px;
width: 24px;
border-radius: 6px;
}
.placeholder.large {
width: 48px;
}
.placeholder.xlarge {
width: 64px;
}
.placeholder.xxlarge {
margin: 7.2px 0;
width: 96px;
}
h2 .placeholder {
background-color: rgba(3,102,214,.62);
}
<%= style %>
/* Avatar */
.avatar {
background-color: rgba(119,119,119,.62);
border-radius: 50%;
margin: 0 6px;
height: 20px;
width: 20px;
}
/* Placeholder */
.placeholder {
background-color: rgba(119,119,119,.62);
margin: 2px 4px 0px;
height: 12px;
width: 24px;
border-radius: 6px;
}
.placeholder.large {
width: 48px;
}
.placeholder.xlarge {
width: 64px;
}
.placeholder.xxlarge {
margin: 7.2px 0;
width: 96px;
}
.placeholder.inline {
display: inline-block;
}
h2 .placeholder {
background-color: rgba(3,102,214,.62);
}
</style>
<foreignObject x="0" y="0" width="100%" height="100%">
@@ -257,6 +165,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5A1.75 1.75 0 0114.25 16H1.75A1.75 1.75 0 010 14.25V1.75zm9.22 3.72a.75.75 0 000 1.06L10.69 8 9.22 9.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM6.78 6.53a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 101.06-1.06L5.31 8l1.47-1.47z"/></svg>
<div class="placeholder"></div> Gists
</div>
<% if (plugins.lines) { %>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>
<div class="placeholder"></div> added, <div class="placeholder"></div> removed
</div>
<% } %>
</section>
<section>
@@ -272,93 +186,167 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"/></svg>
<div class="placeholder"></div> Watchers
</div>
<% if (plugins.traffic) { %>
<div class="field">
<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>
<div class="placeholder"></div> views in last two weeks
</div>
<% } %>
</section>
</div>
</section>
<div class="row">
<% if (plugins.followup) { %>
<div class="row">
<section class="column">
<h3>Issues</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="220" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"/></svg>
<div class="placeholder"></div> Closed
<section class="column">
<h3>Issues</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="issues-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#issues-bar)" x="0" y="0" width="220" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d73a49" fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 0110.65-5.003.75.75 0 00.959-1.153 8 8 0 102.592 8.33.75.75 0 10-1.444-.407A6.5 6.5 0 011.5 8zM8 12a1 1 0 100-2 1 1 0 000 2zm0-8a.75.75 0 01.75.75v3.5a.75.75 0 11-1.5 0v-3.5A.75.75 0 018 4zm4.78 4.28l3-3a.75.75 0 00-1.06-1.06l-2.47 2.47-.97-.97a.749.749 0 10-1.06 1.06l1.5 1.5a.75.75 0 001.06 0z"/></svg>
<div class="placeholder"></div> Closed
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" 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 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/></svg>
<div class="placeholder"></div> Open
</div>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" 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 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"/></svg>
<div class="placeholder"></div> Open
</div>
</div>
</section>
</section>
<section class="column">
<h3>Pull requests</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="220" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"/></svg>
<div class="placeholder"></div> Merged
<section class="column">
<h3>Pull requests</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="220" height="8">
<mask id="pr-bar">
<rect x="0" y="0" width="220" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#pr-bar)" x="0" y="0" width="220" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal fill-width">
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#6f42c1" fill-rule="evenodd" d="M5 3.254V3.25v.005a.75.75 0 110-.005v.004zm.45 1.9a2.25 2.25 0 10-1.95.218v5.256a2.25 2.25 0 101.5 0V7.123A5.735 5.735 0 009.25 9h1.378a2.251 2.251 0 100-1.5H9.25a4.25 4.25 0 01-3.8-2.346zM12.75 9a.75.75 0 100-1.5.75.75 0 000 1.5zm-8.5 4.5a.75.75 0 100-1.5.75.75 0 000 1.5z"/></svg>
<div class="placeholder"></div> Merged
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/></svg>
<div class="placeholder"></div> Open
</div>
</div>
<div class="field center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#28a745" fill-rule="evenodd" d="M7.177 3.073L9.573.677A.25.25 0 0110 .854v4.792a.25.25 0 01-.427.177L7.177 3.427a.25.25 0 010-.354zM3.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122v5.256a2.251 2.251 0 11-1.5 0V5.372A2.25 2.25 0 011.5 3.25zM11 2.5h-1V4h1a1 1 0 011 1v5.628a2.251 2.251 0 101.5 0V5A2.5 2.5 0 0011 2.5zm1 10.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0zM3.75 12a.75.75 0 100 1.5.75.75 0 000-1.5z"/></svg>
<div class="placeholder"></div> Open
</div>
</div>
</section>
</section>
</div>
<section class="column">
<h3>Most used languages</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="460" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal horizontal-wrap fill-width">
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
</div>
</section>
<% } %>
<% if (plugins.languages) { %>
<section class="column">
<h3>Most used languages</h3>
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
<mask id="languages-bar">
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
</mask>
<rect mask="url(#languages-bar)" x="0" y="0" width="460" height="8" fill="#d1d5da"/>
</svg>
<div class="field horizontal horizontal-wrap fill-width">
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
<div class="field center no-wrap language">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#d1d5da" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"/></svg>
<div class="placeholder xlarge"></div>
</div>
</div>
</section>
<% } %>
<% if (plugins.pagespeed) { %>
<div class="row">
<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="M14.064 0a8.75 8.75 0 00-6.187 2.563l-.459.458c-.314.314-.616.641-.904.979H3.31a1.75 1.75 0 00-1.49.833L.11 7.607a.75.75 0 00.418 1.11l3.102.954c.037.051.079.1.124.145l2.429 2.428c.046.046.094.088.145.125l.954 3.102a.75.75 0 001.11.418l2.774-1.707a1.75 1.75 0 00.833-1.49V9.485c.338-.288.665-.59.979-.904l.458-.459A8.75 8.75 0 0016 1.936V1.75A1.75 1.75 0 0014.25 0h-.186zM10.5 10.625c-.088.06-.177.118-.266.175l-2.35 1.521.548 1.783 1.949-1.2a.25.25 0 00.119-.213v-2.066zM3.678 8.116L5.2 5.766c.058-.09.117-.178.176-.266H3.309a.25.25 0 00-.213.119l-1.2 1.95 1.782.547zm5.26-4.493A7.25 7.25 0 0114.063 1.5h.186a.25.25 0 01.25.25v.186a7.25 7.25 0 01-2.123 5.127l-.459.458a15.21 15.21 0 01-2.499 2.02l-2.317 1.5-2.143-2.143 1.5-2.317a15.25 15.25 0 012.02-2.5l.458-.458h.002zM12 5a1 1 0 11-2 0 1 1 0 012 0zm-8.44 9.56a1.5 1.5 0 10-2.12-2.12c-.734.73-1.047 2.332-1.15 3.003a.23.23 0 00.265.265c.671-.103 2.273-.416 3.005-1.148z"></path></svg>
PageSpeed Insights
</h2>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg>
<div class="placeholder xlarge"></div>
</div>
</section>
</div>
<section>
<div class="row fill-width">
<section class="categories">
<div class="categorie column">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="50" height="50" class="gauge">
<circle class="gauge-base" r="53" cx="60" cy="60"></circle>
<text x="60" y="60" dominant-baseline="central" >-</text>
</svg>
<span class="title">Performance</span>
</div>
<div class="categorie column">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="50" height="50" class="gauge">
<circle class="gauge-base" r="53" cx="60" cy="60"></circle>
<text x="60" y="60" dominant-baseline="central" >-</text>
</svg>
<span class="title">Accessibility</span>
</div>
<div class="categorie column">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="50" height="50" class="gauge">
<circle class="gauge-base" r="53" cx="60" cy="60"></circle>
<text x="60" y="60" dominant-baseline="central" >-</text>
</svg>
<span class="title">Best Practices</span>
</div>
<div class="categorie column">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="50" height="50" class="gauge">
<circle class="gauge-base" r="53" cx="60" cy="60"></circle>
<text x="60" y="60" dominant-baseline="central" >-</text>
</svg>
<span class="title">SEO</span>
</div>
</section>
</div>
</section>
<% } %>
<% if (plugins.habits) { %>
<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="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 01-1.484.211c-.04-.282-.163-.547-.37-.847a8.695 8.695 0 00-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.75.75 0 01-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75zM6 15.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5h-2.5a.75.75 0 01-.75-.75zM5.75 12a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5z"></path></svg>
Coding habits
</h2>
<div class="row">
<ul class="habits">
<li>Use <div class="placeholder inline large"></div> for indents</li>
<li>Mostly push code around <div class="placeholder inline"></div></li>
</ul>
</div>
</section>
<% } %>
</div>
</foreignObject>

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -2,9 +2,7 @@
export default async function ({login, q}, {data, rest, graphql, plugins}, {s, pending, imports}) {
//Init
const languages = {colors:{}, total:0, stats:{}}
const licenses = {favorite:"", used:{}}
const computed = data.computed = {commits:0, languages, licenses, svg:{height:505, width:480}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}, plugins:{}}
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, svg:{height:355, width:480}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0}, plugins:{}}
const avatar = imports.imgb64(data.user.avatarUrl)
//Plugins
@@ -14,6 +12,8 @@
imports.plugins.traffic({login, repositories:data.user.repositories.nodes.map(({name}) => name), rest, computed, pending, q}, plugins.traffic)
imports.plugins.habits({login, rest, computed, pending, q}, plugins.habits)
imports.plugins.selfskip({login, rest, computed, pending, q}, plugins.selfskip)
imports.plugins.languages({login, data, computed, pending, q}, plugins.languages)
imports.plugins.followup({login, data, computed, pending, q}, plugins.followup)
//Iterate through user's repositories
for (const repository of data.user.repositories.nodes) {
@@ -22,23 +22,16 @@
computed.repositories[property] += repository[property].totalCount
//Forks
computed.repositories.forks += repository.forkCount
//Languages
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
languages.stats[name] = (languages.stats[name] || 0) + size
languages.colors[name] = color || "#ededed"
languages.total += size
}
//License
if (repository.licenseInfo)
licenses.used[repository.licenseInfo.spdxId] = (licenses.used[repository.licenseInfo.spdxId] || 0) + 1
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] || 0) + 1
}
//Compute count for issues and pull requests
for (const property of ["issues", "pr"])
computed.repositories[`${property}_count`] = computed.repositories[`${property}_open`] + computed.repositories[`${property}_${property === "pr" ? "merged" : "closed"}`]
//Compute licenses stats
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || ""
//Compute total commits and sponsorships
computed.commits = data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
computed.sponsorships = data.user.sponsorshipsAsSponsor.totalCount + data.user.sponsorshipsAsMaintainer.totalCount
//Compute registration date
@@ -47,15 +40,6 @@
const months = Math.ceil((diff-years)*12)
computed.registration = years ? `${years} year${s(years)} ago` : `${months} month${s(months)} ago`
//Compute languages stats
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
for (let i = 1; i < languages.favorites.length; i++)
languages.favorites[i].x = languages.favorites[i-1].x + languages.favorites[i-1].value
//Compute licenses stats
licenses.favorite = Object.entries(licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || ""
//Compute calendar
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()