refactor(metrics/insights): new features (#1098)

This commit is contained in:
Simon Lecoq
2022-06-24 22:35:09 +02:00
committed by GitHub
parent 79b80b1900
commit 3c4402e4ba
10 changed files with 732 additions and 142 deletions

View File

@@ -4,7 +4,7 @@ import util from "util"
import * as utils from "./utils.mjs" import * as utils from "./utils.mjs"
//Setup //Setup
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) { export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null, callbacks = null}, {Plugins, Templates}) {
//Compute rendering //Compute rendering
try { try {
//Debug //Debug
@@ -59,8 +59,8 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Executing base plugin and compute metrics //Executing base plugin and compute metrics
console.debug(`metrics/compute/${login} > compute`) console.debug(`metrics/compute/${login} > compute`)
await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf) await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports, callbacks}, conf)
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account: data.account, convert, template}, {pending, imports}) await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account: data.account, convert, template, callbacks}, {pending, imports})
const promised = await Promise.all(pending) const promised = await Promise.all(pending)
//Check plugins errors //Check plugins errors
@@ -211,8 +211,10 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
} }
//Metrics insights //Metrics insights
metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) { metrics.insights = async function({login}, {graphql, rest, conf, callbacks}, {Plugins, Templates}) {
const q = { return metrics({login, q:metrics.insights.q}, {graphql, rest, plugins:metrics.insights.plugins, conf, callbacks, convert: "json"}, {Plugins, Templates})
}
metrics.insights.q = {
template: "classic", template: "classic",
achievements: true, achievements: true,
"achievements.threshold": "X", "achievements.threshold": "X",
@@ -223,27 +225,39 @@ metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Temp
activity: true, activity: true,
"activity.limit": 100, "activity.limit": 100,
"activity.days": 0, "activity.days": 0,
"activity.timestamps": true,
notable: true, notable: true,
"notable.repositories": true,
followup: true, followup: true,
"followup.sections": "repositories, user", "followup.sections": "repositories, user",
habits: true,
"habits.from": 100,
"habits.days": 7,
"habits.facts": false,
"habits.charts": true,
introduction: true, introduction: true,
} topics: true,
const plugins = { "topics.mode": "icons",
"topics.limit": 0,
stars: true,
"stars.limit": 6,
reactions: true,
"reactions.details": "percentage",
repositories: true,
"repositories.pinned": 6,
sponsors: true,
calendar: true,
"calendar.limit": 0,
}
metrics.insights.plugins = {
achievements: {enabled: true}, achievements: {enabled: true},
isocalendar: {enabled: true}, isocalendar: {enabled: true},
languages: {enabled: true, extras: false}, languages: {enabled: true, extras: false},
activity: {enabled: true, markdown: "extended"}, activity: {enabled: true, markdown: "extended"},
notable: {enabled: true}, notable: {enabled: true},
followup: {enabled: true}, followup: {enabled: true},
habits: {enabled: true, extras: false},
introduction: {enabled: true}, introduction: {enabled: true},
} topics: {enabled: true},
return metrics({login, q}, {graphql, rest, plugins, conf, convert: "json"}, {Plugins, Templates}) stars: {enabled: true},
reactions: {enabled: true},
repositories: {enabled: true},
sponsors: {enabled: true},
calendar: {enabled: true},
} }
//Metrics insights static render //Metrics insights static render
@@ -278,5 +292,5 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
</body> </body>
</html>` </html>`
await browser.close() await browser.close()
return {mime: "text/html", rendered} return {mime: "text/html", rendered, errors:json.errors}
} }

View File

@@ -172,11 +172,32 @@ export default async function({sandbox = false} = {}) {
} }
}) })
//Pending requests
const pending = new Map()
//About routes //About routes
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`)) app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`)) app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`)) app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`)) app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
app.get("/about/query/:login/:plugin/", async (req, res) => {
//Check username
const login = req.params.login?.replace(/[\n\r]/g, "")
if (!/^[-\w]+$/i.test(login)) {
console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
return res.status(400).send("Bad request: username seems invalid")
}
//Check plugin
const plugin = req.params.plugin?.replace(/[\n\r]/g, "")
if (!/^\w+$/i.test(plugin)) {
console.debug(`metrics/app/${login}/insights > 400 (invalid plugin name)`)
return res.status(400).send("Bad request: plugin name seems invalid")
}
if (cache.get(`about.${login}.${plugin}`)) {
return res.send(cache.get(`about.${login}.${plugin}`))
}
return res.status(204).send("No content: no data fetched yet")
})
app.get("/about/query/:login/", ...middlewares, async (req, res) => { app.get("/about/query/:login/", ...middlewares, async (req, res) => {
//Check username //Check username
const login = req.params.login?.replace(/[\n\r]/g, "") const login = req.params.login?.replace(/[\n\r]/g, "")
@@ -185,7 +206,16 @@ export default async function({sandbox = false} = {}) {
return res.status(400).send("Bad request: username seems invalid") return res.status(400).send("Bad request: username seems invalid")
} }
//Compute metrics //Compute metrics
let solve = null
try { try {
//Prevent multiples requests
if ((!debug) && (!mock) && (pending.has(`about.${login}`))) {
console.debug(`metrics/app/${login}/insights > awaiting pending request`)
await pending.get(`about.${login}`)
}
else {
pending.set(`about.${login}`, new Promise(_solve => solve = _solve))
}
//Read cached data if possible //Read cached data if possible
if ((!debug) && (cached) && (cache.get(`about.${login}`))) { if ((!debug) && (cached) && (cache.get(`about.${login}`))) {
console.debug(`metrics/app/${login}/insights > using cached results`) console.debug(`metrics/app/${login}/insights > using cached results`)
@@ -193,13 +223,28 @@ export default async function({sandbox = false} = {}) {
} }
//Compute metrics //Compute metrics
console.debug(`metrics/app/${login}/insights > compute insights`) console.debug(`metrics/app/${login}/insights > compute insights`)
const json = await metrics.insights({login}, {graphql, rest, conf}, {Plugins, Templates}) const callbacks = {
async plugin(login, plugin, success, result) {
console.debug(`metrics/app/${login}/insights/plugins > ${plugin} > ${success ? "success" : "failure"}`)
cache.put(`about.${login}.${plugin}`, result)
}
}
;(async () => {
try {
const json = await metrics.insights({login}, {graphql, rest, conf, callbacks}, {Plugins, Templates})
//Cache //Cache
cache.put(`about.${login}`, json)
if ((!debug) && (cached)) { if ((!debug) && (cached)) {
const maxage = Math.round(Number(req.query.cache)) const maxage = Math.round(Number(req.query.cache))
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached) cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
} }
return res.json(json) }
catch (error) {
console.error(`metrics/app/${login}/insights > error > ${error}`)
}
})()
console.debug(`metrics/app/${login}/insights > accepted request`)
return res.status(202).json({processing:true, plugins:Object.keys(metrics.insights.plugins)})
} }
//Internal error //Internal error
catch (error) { catch (error) {
@@ -219,12 +264,12 @@ export default async function({sandbox = false} = {}) {
return res.status(500).send("Internal Server Error: failed to process metrics correctly") return res.status(500).send("Internal Server Error: failed to process metrics correctly")
} }
finally { finally {
solve?.()
_requests_refresh = true _requests_refresh = true
} }
}) })
//Metrics //Metrics
const pending = new Map()
app.get("/:login/:repository?", ...middlewares, async (req, res) => { app.get("/:login/:repository?", ...middlewares, async (req, res) => {
//Request params //Request params
const login = req.params.login?.replace(/[\n\r]/g, "") const login = req.params.login?.replace(/[\n\r]/g, "")

View File

@@ -22,6 +22,10 @@
<a href="https://github.com/lowlighter/metrics">Metrics Insights {{ version }}</a> <a href="https://github.com/lowlighter/metrics">Metrics Insights {{ version }}</a>
</header> </header>
<div class="loading-bar" v-if="(progress > 0)&&(1 > progress)">
<div :style="{width:`${progress*100}%`}"></div>
</div>
<section class="container center"> <section class="container center">
<div class="search" v-if="searchable"> <div class="search" v-if="searchable">
<div class="about"> <div class="about">
@@ -81,10 +85,15 @@
</a> </a>
</section> </section>
<section class="container text-center"> <section class="container text-center" v-if="loaded.includes('introduction')">
{{ introduction }} {{ introduction }}
</section> </section>
<section class="container text-center" v-if="!loaded.includes('achievements')">
<div class="loading">Fetching achievements</div>
</section>
<template v-else>
<div class="rankeds"> <div class="rankeds">
<div v-for="{icon, prefix, title, text, rank, progress, value, leaderboard = null} in ranked" class="ranked" :class="{[rank.charAt(0).toLocaleLowerCase()]:rank !== '$', secret:rank === '$'}"> <div v-for="{icon, prefix, title, text, rank, progress, value, leaderboard = null} in ranked" class="ranked" :class="{[rank.charAt(0).toLocaleLowerCase()]:rank !== '$', secret:rank === '$'}">
<div class="icon"> <div class="icon">
@@ -110,24 +119,7 @@
</div> </div>
</div> </div>
<section class="container" v-if="(account.type === 'user')&&(contributions.length)">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.5A2.5 2.5 0 013.5 0h8.75a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V1.5h-8a1 1 0 00-1 1v6.708A2.492 2.492 0 013.5 9h3.25a.75.75 0 010 1.5H3.5a1 1 0 100 2h5.75a.75.75 0 010 1.5H3.5A2.5 2.5 0 011 11.5v-9zm13.23 7.79a.75.75 0 001.06-1.06l-2.505-2.505a.75.75 0 00-1.06 0L9.22 9.229a.75.75 0 001.06 1.061l1.225-1.224v6.184a.75.75 0 001.5 0V9.066l1.224 1.224z"></path></svg>
Notable contributions
</h2>
<div class="contributions">
<a v-for="{name, avatar} in contributions" class="contribution" :href="`https://github.com/${name}`">
<img :src="avatar" alt="">
@{{ name }}
</a>
</div>
</section>
<section class="container"> <section class="container">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8.5.75a.75.75 0 00-1.5 0v5.19L4.391 3.33a.75.75 0 10-1.06 1.061L5.939 7H.75a.75.75 0 000 1.5h5.19l-2.61 2.609a.75.75 0 101.061 1.06L7 9.561v5.189a.75.75 0 001.5 0V9.56l2.609 2.61a.75.75 0 101.06-1.061L9.561 8.5h5.189a.75.75 0 000-1.5H9.56l2.61-2.609a.75.75 0 00-1.061-1.06L8.5 5.939V.75z"></path></svg>
Highlights
</h2>
<div class="achievements"> <div class="achievements">
<div v-for="{icon, prefix, title, text, rank, progress, value, leaderboard = null} in achievements" class="achievement" :class="{[rank.charAt(0).toLocaleLowerCase()]:rank !== '$', secret:rank === '$'}"> <div v-for="{icon, prefix, title, text, rank, progress, value, leaderboard = null} in achievements" class="achievement" :class="{[rank.charAt(0).toLocaleLowerCase()]:rank !== '$', secret:rank === '$'}">
<div class="icon"> <div class="icon">
@@ -152,34 +144,222 @@
</div> </div>
</section> </section>
</template>
<section class="container" v-if="(loaded.includes('sponsors'))&&(sponsors)&&(!sponsors.error)">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>
Sponsor {{ user }}
</h2>
<div v-html="sponsors.about" class="markdown"></div>
<div class="goal" v-if="(sponsors.goal)||((sponsors.list)&&(sponsors.list.length))">
<template v-if="sponsors.goal">
<p>{{ sponsors.goal.description }}</p>
<div class="progress-wrap">
<div class="progress" :style="{width:`${sponsors.goal.progress}%`, backgroundColor:'#EC6CB9'}"></div>
</div>
<div class="objective">{{ sponsors.goal.title }}</div>
</template>
<template v-if="(sponsors.list)&&(sponsors.list.length)">
<p>{{ sponsors.list.length }} sponsors are funding <b>{{ user }}</b>'s work:</p>
<div class="sponsors-list">
<img v-for="{avatar, login} in sponsors.list" :src="avatar" :alt="login" :title="login" class="avatar">
</div>
</template>
</div>
</section>
<section class="container" v-if="account.type === 'user'">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.5A2.5 2.5 0 013.5 0h8.75a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0V1.5h-8a1 1 0 00-1 1v6.708A2.492 2.492 0 013.5 9h3.25a.75.75 0 010 1.5H3.5a1 1 0 100 2h5.75a.75.75 0 010 1.5H3.5A2.5 2.5 0 011 11.5v-9zm13.23 7.79a.75.75 0 001.06-1.06l-2.505-2.505a.75.75 0 00-1.06 0L9.22 9.229a.75.75 0 001.06 1.061l1.225-1.224v6.184a.75.75 0 001.5 0V9.066l1.224 1.224z"></path></svg>
Notable contributions
</h2>
<div class="loading" v-if="!loaded.includes('notable')">Fetching data</div>
<template v-else-if="contributions.length">
<p>
<b>{{ user }}</b> has contributed to {{ format("number", stats?.repositoriesContributedTo?.totalCount) }} repositor{{ format("plural", stats?.repositoriesContributedTo?.totalCount, {y:true}) }}.
</p>
<div class="contributions">
<a v-for="{name, avatar} in contributions" class="contribution" :href="`https://github.com/${name}`">
<img :src="avatar" alt="">
@{{ name }}
</a>
</div>
</template>
<div v-else>
No contributions to display
</div>
</section>
<section class="container">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 2.75A2.75 2.75 0 012.75 0h10.5A2.75 2.75 0 0116 2.75v10.5A2.75 2.75 0 0113.25 16H2.75A2.75 2.75 0 010 13.25V2.75zM2.75 1.5c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25V2.75c0-.69-.56-1.25-1.25-1.25H2.75z"></path><path d="M8 4a.75.75 0 01.75.75V6.7l1.69-.975a.75.75 0 01.75 1.3L9.5 8l1.69.976a.75.75 0 01-.75 1.298L8.75 9.3v1.951a.75.75 0 01-1.5 0V9.299l-1.69.976a.75.75 0 01-.75-1.3L6.5 8l-1.69-.975a.75.75 0 01.75-1.3l1.69.976V4.75A.75.75 0 018 4z"></path></svg>
Featured repositories
</h2>
<div class="loading" v-if="!loaded.includes('repositories')">Fetching data</div>
<div v-else-if="repositories.length" class="repositories">
<div class="repository" v-for="repository in repositories">
<div class="field">
<svg v-if="repository.isFork" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<svg v-else 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>
<div class="name">
<span>{{ repository.nameWithOwner }}</span>
<span v-if="repository.created">created {{ repository.created }}</span>
<span v-if="repository.starredAt">starred on {{ format("date", repository.starredAt) }}</span>
</div>
</div>
<div class="field description">{{ repository.description }}</div>
<div class="field infos">
<div class="language" v-if="repository.primaryLanguage">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path :fill="repository.primaryLanguage.color" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
{{ repository.primaryLanguage.name }}
</div>
<div v-if="repository.licenseInfo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.75.75a.75.75 0 00-1.5 0V2h-.984c-.305 0-.604.08-.869.23l-1.288.737A.25.25 0 013.984 3H1.75a.75.75 0 000 1.5h.428L.066 9.192a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.514 3.514 0 00.686.45A4.492 4.492 0 003 11c.88 0 1.556-.22 2.023-.454a3.515 3.515 0 00.686-.45l.045-.04.016-.015.006-.006.002-.002.001-.002L5.25 9.5l.53.53a.75.75 0 00.154-.838L3.822 4.5h.162c.305 0 .604-.08.869-.23l1.289-.737a.25.25 0 01.124-.033h.984V13h-2.5a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-2.5V3.5h.984a.25.25 0 01.124.033l1.29.736c.264.152.563.231.868.231h.162l-2.112 4.692a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.517 3.517 0 00.686.45A4.492 4.492 0 0013 11c.88 0 1.556-.22 2.023-.454a3.512 3.512 0 00.686-.45l.045-.04.01-.01.006-.005.006-.006.002-.002.001-.002-.529-.531.53.53a.75.75 0 00.154-.838L13.823 4.5h.427a.75.75 0 000-1.5h-2.234a.25.25 0 01-.124-.033l-1.29-.736A1.75 1.75 0 009.735 2H8.75V.75zM1.695 9.227c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327l-1.305 2.9zm10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327l-1.305 2.9z"></path></svg>
{{ repository.licenseInfo.name ?? repository.licenseInfo.spdxId }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
{{ format("number", repository.stargazerCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
{{ format("number", repository.forkCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path><path fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"></path></svg>
{{ format("number", repository.issues.totalCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path 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>
{{ format("number", repository.pullRequests.totalCount) }}
</div>
</div>
</div>
</div>
<div v-else>
No featured repositories
</div>
</section>
<section class="container" v-if="account.type === 'user'">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 2.75a.25.25 0 01.25-.25h8.5a.25.25 0 01.25.25v5.5a.25.25 0 01-.25.25h-3.5a.75.75 0 00-.53.22L3.5 11.44V9.25a.75.75 0 00-.75-.75h-1a.25.25 0 01-.25-.25v-5.5zM1.75 1A1.75 1.75 0 000 2.75v5.5C0 9.216.784 10 1.75 10H2v1.543a1.457 1.457 0 002.487 1.03L7.061 10h3.189A1.75 1.75 0 0012 8.25v-5.5A1.75 1.75 0 0010.25 1h-8.5zM14.5 4.75a.25.25 0 00-.25-.25h-.5a.75.75 0 110-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0114.25 12H14v1.543a1.457 1.457 0 01-2.487 1.03L9.22 12.28a.75.75 0 111.06-1.06l2.22 2.22v-2.19a.75.75 0 01.75-.75h1a.25.25 0 00.25-.25v-5.5z"></path></svg>
Overall users reactions
</h2>
<div class="loading" v-if="!loaded.includes('reactions')">Fetching data</div>
<template v-else-if="reactions">
<p>
<b>{{ user }}</b> has collected {{ format("number", reactions.total) }} reaction{{ format("plural", reactions.total) }} from other users over their last {{ format("number", reactions.comments) }} comment{{ format("plural", reactions.comments) }}.
</p>
<div class="reactions">
<div v-for="({score, value}, reaction) in reactions.list">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" height="66" width="66" class="gauge">
<circle class="gauge-base" r="25" cx="28" cy="28"></circle>
<circle v-if="score" class="gauge-arc" transform="rotate(-90 28 28)" r="25" cx="28" cy="28" :stroke-dasharray="`${score*155} 155`"></circle>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle">{{ {HEART:'❤️', THUMBS_UP:'👍', THUMBS_DOWN:'👎', LAUGH:'😄', CONFUSED:'😕', EYES:'👀', ROCKET:'🚀', HOORAY:'🎉'}[reaction] }}</text>
</svg>
<div class="text">{{ value }} time{{ format("plural", value) }}</div>
<div class="text">{{ (score*100).toFixed(2) }}<small>%</small></div>
</div>
</div>
</template>
<div v-else>
No users reactions
</div>
</section>
<section class="container" v-if="account.type === 'user'">
<div class="topics">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M14.184 1.143a1.75 1.75 0 00-2.502-.57L.912 7.916a1.75 1.75 0 00-.53 2.32l.447.775a1.75 1.75 0 002.275.702l11.745-5.656a1.75 1.75 0 00.757-2.451l-1.422-2.464zm-1.657.669a.25.25 0 01.358.081l1.422 2.464a.25.25 0 01-.108.35l-2.016.97-1.505-2.605 1.85-1.26zM9.436 3.92l1.391 2.41-5.42 2.61-.942-1.63 4.97-3.39zM3.222 8.157l-1.466 1a.25.25 0 00-.075.33l.447.775a.25.25 0 00.325.1l1.598-.769-.83-1.436zm6.253 2.306a.75.75 0 00-.944-.252l-1.809.87a.75.75 0 00-.293.253L4.38 14.326a.75.75 0 101.238.848l1.881-2.75v2.826a.75.75 0 001.5 0v-2.826l1.881 2.75a.75.75 0 001.238-.848l-2.644-3.863z"></path></svg>
Starred topics
</h2>
<div class="loading" v-if="!loaded.includes('topics')">Fetching data</div>
<template v-else-if="topics.length">
<p>
<b>{{ user }}</b> has starred {{ format("number", topics.length) }} topic{{ format("plural", topics.length) }}.
</p>
<div class="list">
<a v-for="{name, description, icon, url} of topics" class="topic" :title="description" :href="url">
<img v-if="icon" :src="icon" alt="">
{{ name }}
</a>
</div>
</template>
<div v-else>
No starred topics.
</div>
</div>
</section>
<section class="container"> <section class="container">
<div class="languages"> <div class="languages">
<h2> <h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 2.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v8.5a.25.25 0 01-.25.25h-6.5a.75.75 0 00-.53.22L4.5 14.44v-2.19a.75.75 0 00-.75-.75h-2a.25.25 0 01-.25-.25v-8.5zM1.75 1A1.75 1.75 0 000 2.75v8.5C0 12.216.784 13 1.75 13H3v1.543a1.457 1.457 0 002.487 1.03L8.061 13h6.189A1.75 1.75 0 0016 11.25v-8.5A1.75 1.75 0 0014.25 1H1.75zm5.03 3.47a.75.75 0 010 1.06L5.31 7l1.47 1.47a.75.75 0 01-1.06 1.06l-2-2a.75.75 0 010-1.06l2-2a.75.75 0 011.06 0zm2.44 0a.75.75 0 000 1.06L10.69 7 9.22 8.47a.75.75 0 001.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 2.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v8.5a.25.25 0 01-.25.25h-6.5a.75.75 0 00-.53.22L4.5 14.44v-2.19a.75.75 0 00-.75-.75h-2a.25.25 0 01-.25-.25v-8.5zM1.75 1A1.75 1.75 0 000 2.75v8.5C0 12.216.784 13 1.75 13H3v1.543a1.457 1.457 0 002.487 1.03L8.061 13h6.189A1.75 1.75 0 0016 11.25v-8.5A1.75 1.75 0 0014.25 1H1.75zm5.03 3.47a.75.75 0 010 1.06L5.31 7l1.47 1.47a.75.75 0 01-1.06 1.06l-2-2a.75.75 0 010-1.06l2-2a.75.75 0 011.06 0zm2.44 0a.75.75 0 000 1.06L10.69 7 9.22 8.47a.75.75 0 001.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0z"></path></svg>
Languages used Used languages
</h2> </h2>
<div class="list" v-if="languages.length"> <div class="loading" v-if="!loaded.includes('languages')">Fetching data</div>
<template v-else-if="languages.length">
<p>
<b>{{ user }}</b> has used {{ languages.length }} different language{{ format("plural", languages.length) }} for a total of {{ format("number", languages.total) }} byte{{ format("plural", languages.total) }}.
<small class="footnote">
Note that these numbers are based on results produced by <a href="github.com/github/linguist">GitHub linguist</a> from repositories owned by <b>{{ user }}</b> and that they may not be entirely accurate.
</small>
</p>
<div class="list">
<div class="language" v-for="{name, value, color, size} of languages"> <div class="language" v-for="{name, value, color, size} of languages">
<div class="progress" :style="{width:`${100*value}%`, backgroundColor:color}"></div> <div class="progress" :style="{width:`${100*value}%`, backgroundColor:color}"></div>
<div :style="{color}"> <div :style="{color}">
<span class="name">{{ name }}</span> <span class="name">{{ name }}</span>
<span class="percent">({{ format("number", value, {style:"percent", maximumFractionDigits:2})}})</span> <span class="percent">({{ format("number", value, {style:"percent", maximumFractionDigits:2})}})</span>
</div> </div>
<div class="size">{{ format("number", size) }} byte{{ "s" }}</div> <div class="size">{{ format("number", size) }} byte{{ format("plural", size) }}</div>
</div> </div>
</div> </div>
</template>
<div v-else> <div v-else>
No languages used No languages used
</div> </div>
</div> </div>
</section> </section>
<section class="container" v-if="followup"> <section class="container" v-if="account.type === 'user'">
<div class="isocalendar">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z"></path></svg>
Commits history
</h2>
<div class="loading" v-if="!loaded.includes('isocalendar')">Fetching isometric calendar</div>
<div class="svg" v-else v-html="isocalendar"></div>
</div>
<div class="loading" v-if="!loaded.includes('calendar')">Fetching data</div>
<template v-else-if="(calendar)&&(calendar.years?.length > 1)">
<p>
Below is the full commit history of <b>{{ user }}</b> between {{ calendar.years[0].year }} and {{ calendar.years[calendar.years.length-1].year }}.
<small class="footnote">
Note that commits prior <b>{{ user }}</b>'s registration date on GitHub are not displayed.
</small>
</p>
<div v-for="[r, {year, weeks}] of Object.entries(calendar.years)">
<svg class="calendar" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0,0 795,108">
<g v-for="[x, week] of Object.entries(weeks)" :transform="`translate(${x*15}, 0)`">
<rect v-for="[y, {date, contributionCount, color}] of Object.entries(week.contributionDays)" class="day" x="0" :y="4 + (x == 0)*(7-week.contributionDays.length)*15 + y*15" width="11" height="11" :fill="calendar.color(color)" rx="2" ry="2">
<title>{{ date }}: {{ contributionCount }} commit{{ format("plural", contributionCount) }}</title>
</rect>
</g>
</svg>
</div>
</template>
</section>
<section class="container">
<h2> <h2>
<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 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> <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 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>
Overall status of related issues and pull requests Overall status of related issues and pull requests
</h2> </h2>
<div class="followup"> <div class="loading" v-if="!loaded.includes('followup')">Fetching data</div>
<div v-else-if="followup" class="followup">
<template v-for="{type, section} in [{type:'repositories', section:followup}, {type:'user', section:followup.user||{}}]"> <template v-for="{type, section} in [{type:'repositories', section:followup}, {type:'user', section:followup.user||{}}]">
<div class="followup-section" v-if="section.issues"> <div class="followup-section" v-if="section.issues">
<h3>Issues {{ {repositories:`opened on ${account.login}'${[...account.login].pop() === "s" ? "" : "s"} repositories`, user:`opened by ${account.login}`}[type] || "" }}</h3> <h3>Issues {{ {repositories:`opened on ${account.login}'${[...account.login].pop() === "s" ? "" : "s"} repositories`, user:`opened by ${account.login}`}[type] || "" }}</h3>
@@ -222,29 +402,59 @@
</div> </div>
</template> </template>
</div> </div>
<div v-else>
No available data
</div>
</section> </section>
<section class="container" v-if="account.type === 'user'"> <section class="container" v-if="account.type === 'user'">
<div class="isocalendar">
<h2> <h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 0a.75.75 0 01.75.75V2h5V.75a.75.75 0 011.5 0V2h1.25c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V3.75C1 2.784 1.784 2 2.75 2H4V.75A.75.75 0 014.75 0zm0 3.5h8.5a.25.25 0 01.25.25V6h-11V3.75a.25.25 0 01.25-.25h2zm-2.25 4v6.75c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V7.5h-11z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
Commits calendar Recently starred repositories
</h2> </h2>
<div class="svg" v-html="isocalendar"></div> <div class="loading" v-if="!loaded.includes('stars')">Fetching data</div>
<div v-else-if="stars.repositories.length" class="repositories">
<div class="repository" v-for="repository in stars.repositories">
<div class="field">
<svg v-if="repository.isFork" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
<svg v-else 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>
<div class="name">
<span>{{ repository.nameWithOwner }}</span>
<span v-if="repository.created">created {{ repository.created }}</span>
<span v-if="repository.starredAt">starred on {{ format("date", repository.starredAt) }}</span>
</div> </div>
</section>
<section class="container" v-if="(account.type === 'user')&&(habits)">
<h2>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6 2a.75.75 0 01.696.471L10 10.731l1.304-3.26A.75.75 0 0112 7h3.25a.75.75 0 010 1.5h-2.742l-1.812 4.528a.75.75 0 01-1.392 0L6 4.77 4.696 8.03A.75.75 0 014 8.5H.75a.75.75 0 010-1.5h2.742l1.812-4.529A.75.75 0 016 2z"></path></svg>
Average commits at each hour over the last week
</h2>
<div class="chart-bars">
<div class="entry" v-for="h in 24">
<span class="value">{{ habits[h] }}</span>
<div class="bar" :style="{height:`${((habits[h]||0)/(habits.max||1))*150}px`, backgroundColor:`var(--color-calendar-graph-day-L${Math.ceil(((habits[h]||0)/(habits.max||1))/0.25)}-bg)`}"></div>
<span class="label">{{ `${h}`.padStart(2, 0) }}</span>
</div> </div>
<div class="field description">{{ repository.description }}</div>
<div class="field infos">
<div class="language" v-if="repository.primaryLanguage">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path :fill="repository.primaryLanguage.color" fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>
{{ repository.primaryLanguage.name }}
</div>
<div v-if="repository.licenseInfo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.75.75a.75.75 0 00-1.5 0V2h-.984c-.305 0-.604.08-.869.23l-1.288.737A.25.25 0 013.984 3H1.75a.75.75 0 000 1.5h.428L.066 9.192a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.514 3.514 0 00.686.45A4.492 4.492 0 003 11c.88 0 1.556-.22 2.023-.454a3.515 3.515 0 00.686-.45l.045-.04.016-.015.006-.006.002-.002.001-.002L5.25 9.5l.53.53a.75.75 0 00.154-.838L3.822 4.5h.162c.305 0 .604-.08.869-.23l1.289-.737a.25.25 0 01.124-.033h.984V13h-2.5a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-2.5V3.5h.984a.25.25 0 01.124.033l1.29.736c.264.152.563.231.868.231h.162l-2.112 4.692a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.517 3.517 0 00.686.45A4.492 4.492 0 0013 11c.88 0 1.556-.22 2.023-.454a3.512 3.512 0 00.686-.45l.045-.04.01-.01.006-.005.006-.006.002-.002.001-.002-.529-.531.53.53a.75.75 0 00.154-.838L13.823 4.5h.427a.75.75 0 000-1.5h-2.234a.25.25 0 01-.124-.033l-1.29-.736A1.75 1.75 0 009.735 2H8.75V.75zM1.695 9.227c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327l-1.305 2.9zm10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327l-1.305 2.9z"></path></svg>
{{ repository.licenseInfo.name ?? repository.licenseInfo.spdxId }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z"></path></svg>
{{ format("number", repository.stargazerCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25v-.878a2.25 2.25 0 10-1.5 0v.878a.75.75 0 01-.75.75h-4.5A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"></path></svg>
{{ format("number", repository.forkCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path><path fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"></path></svg>
{{ format("number", repository.issues.totalCount) }}
</div>
<div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path 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>
{{ format("number", repository.pullRequests.totalCount) }}
</div>
</div>
</div>
</div>
<div v-else>
No recently starred repositories
</div> </div>
</section> </section>
@@ -254,7 +464,8 @@
<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> <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>
Recent activity Recent activity
</h2> </h2>
<ul v-if="activity.length"> <div class="loading" v-if="!loaded.includes('activity')">Fetching data</div>
<ul v-else-if="activity.length">
<li v-for="{actor, type, repo, timestamp, ...event} of activity"> <li v-for="{actor, type, repo, timestamp, ...event} of activity">
<time :datetime="timestamp">{{ format("date", timestamp, {timeStyle:"short", dateStyle:"short", timeZone:config.timezone?.name}) }}</time> <time :datetime="timestamp">{{ format("date", timestamp, {timeStyle:"short", dateStyle:"short", timeZone:config.timezone?.name}) }}</time>
<div class="actor" v-if="account.type === 'organization'"> <div class="actor" v-if="account.type === 'organization'">
@@ -267,7 +478,7 @@
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg> <svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 2.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h4.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25H2.75zM1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0113.25 12H9.06l-2.573 2.573A1.457 1.457 0 014 13.543V12H2.75A1.75 1.75 0 011 10.25v-7.5z"></path></svg>
<div class="content"> <div class="content">
Commented on <a :href="`https://github.com/${repo}/${{issue:'issues', pr:'pull', commit:'commit'}[event.on]}/${event.number}`">#{{ event.number }} {{ event.title }}</a> from <a :href="`https://github.com/${repo}`">{{ repo }}</a> Commented on <a :href="`https://github.com/${repo}/${{issue:'issues', pr:'pull', commit:'commit'}[event.on]}/${event.number}`">#{{ event.number }} {{ event.title }}</a> from <a :href="`https://github.com/${repo}`">{{ repo }}</a>
<quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})"></quote> <quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})" class="markdown"></quote>
</div> </div>
</template> </template>
<template v-if="type === 'member'"> <template v-if="type === 'member'">
@@ -282,7 +493,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>
<div class="content"> <div class="content">
{{ event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" }} of <a :href="`https://github.com/${repo}`">{{ repo }}</a> {{ event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" }} of <a :href="`https://github.com/${repo}`">{{ repo }}</a>
<quote v-if="event.content.trim().length" v-html="event.content"></quote> <quote v-if="event.content.trim().length" v-html="event.content" class="markdown"></quote>
</div> </div>
</template> </template>
<template v-if="type === 'fork'"> <template v-if="type === 'fork'">
@@ -306,14 +517,14 @@
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.491 6.491 0 00-5.285 2.715l1.358 1.358A.25.25 0 013.896 6H.25A.25.25 0 010 5.75V2.104a.25.25 0 01.427-.177l1.216 1.216a8 8 0 0114.315 4.03.748.748 0 01-.668.83.75.75 0 01-.824-.676A6.501 6.501 0 008 1.5zM.712 8.004a.75.75 0 01.822.67 6.501 6.501 0 0011.751 3.111l-1.358-1.358a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.216-1.216A8 8 0 01.042 8.827a.75.75 0 01.67-.823zM9 11a1 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> <svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.491 6.491 0 00-5.285 2.715l1.358 1.358A.25.25 0 013.896 6H.25A.25.25 0 010 5.75V2.104a.25.25 0 01.427-.177l1.216 1.216a8 8 0 0114.315 4.03.748.748 0 01-.668.83.75.75 0 01-.824-.676A6.501 6.501 0 008 1.5zM.712 8.004a.75.75 0 01.822.67 6.501 6.501 0 0011.751 3.111l-1.358-1.358a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.216-1.216A8 8 0 01.042 8.827a.75.75 0 01.67-.823zM9 11a1 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>
<div class="content"> <div class="content">
{{ event.action === "opened" ? "Opened" : event.action === "reopened" ? "Reopened" : "Closed" }} <a :href="`https://github.com/${repo}/issues/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a> {{ event.action === "opened" ? "Opened" : event.action === "reopened" ? "Reopened" : "Closed" }} <a :href="`https://github.com/${repo}/issues/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a>
<quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})"></quote> <quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})" class="markdown"></quote>
</div> </div>
</template> </template>
<template v-if="type === 'pr'"> <template v-if="type === 'pr'">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path 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> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path 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>
<div class="content"> <div class="content">
{{ event.action === "opened" ? "Opened" : event.action === "merged" ? "Merged" : "Closed" }} <a :href="`https://github.com/${repo}/pull/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a> {{ event.action === "opened" ? "Opened" : event.action === "merged" ? "Merged" : "Closed" }} <a :href="`https://github.com/${repo}/pull/${event.number}`">#{{ event.number }} {{ event.title }}</a> in <a :href="`https://github.com/${repo}`">{{ repo }}</a>
<quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})"></quote> <quote v-if="event.content.trim().length" v-html="format('comment', event.content, {repo})" class="markdown"></quote>
<ul> <ul>
<li> <li>
{{ event.files.changed }} file{{ "s" }} changed <code>++{{ event.lines.added }} --{{ event.lines.deleted }}</code> {{ event.files.changed }} file{{ "s" }} changed <code>++{{ event.lines.added }} --{{ event.lines.deleted }}</code>

View File

@@ -55,6 +55,10 @@
methods: { methods: {
format(type, value, options) { format(type, value, options) {
switch (type) { switch (type) {
case "plural":
if (options?.y)
return (value !== 1) ? "ies" : "y"
return (value !== 1) ? "s" : ""
case "number": case "number":
return new Intl.NumberFormat(navigator.lang, options).format(value) return new Intl.NumberFormat(navigator.lang, options).format(value)
case "date": case "date":
@@ -74,6 +78,14 @@
RegExp(baseUrl + String.raw`compare\/([\w-.]+...[\w-.]+)(?=<)`, "g"), RegExp(baseUrl + String.raw`compare\/([\w-.]+...[\w-.]+)(?=<)`, "g"),
(_, repo, tags) => (options?.repo === repo ? "" : repo + "@") + tags, (_, repo, tags) => (options?.repo === repo ? "" : repo + "@") + tags,
) // -> 'lowlighter/metrics@1.0...1.1' ) // -> 'lowlighter/metrics@1.0...1.1'
.replace(
/(?<!&)#(\d+)/g,
(_, id) => `<a href="https://github.com/${options?.repo}/issues/${id}">#${id}</a>`,
) // -> #123
.replace(
/@([-\w]+)/g,
(_, user) => `<a href="https://github.com/${user}">@${user}</a>`,
) // -> @user
} }
return value return value
}, },
@@ -86,7 +98,40 @@
this.metrics = JSON.parse(localStorage.getItem("local.metrics") ?? "null") this.metrics = JSON.parse(localStorage.getItem("local.metrics") ?? "null")
return return
} }
this.metrics = (await axios.get(`/about/query/${this.user}`)).data const {processing, ...data} = (await axios.get(`/about/query/${this.user}`)).data
if (processing) {
let completed = 0
this.progress = 1/(data.plugins.length+1)
this.loaded = []
const retry = async (plugin, attempts = 60, interval = 10) => {
if (this.loaded.includes(plugin))
return
do {
try {
const {data} = await axios.get(`/about/query/${this.user}/${plugin}`)
if (!data)
throw new Error(`${plugin}: no data`)
if (plugin === "base")
this.metrics = {rendered:data, mime:"application/json", errors:[]}
else
Object.assign(this.metrics.rendered.plugins, {[plugin]:data})
break
}
catch {
console.warn(`${plugin}: no data yet, retrying in ${interval} seconds`)
await new Promise(solve => setTimeout(solve, interval*1000))
}
} while (--attempts)
completed++
this.progress = completed/(data.plugins.length+1)
this.loaded.push(plugin)
}
await retry("base", 30, 5)
await Promise.allSettled(data.plugins.map(plugin => retry(plugin)))
}
else {
this.metrics = data
}
} }
catch (error) { catch (error) {
this.error = {code: error.response.status, message: error.response.data} this.error = {code: error.response.status, message: error.response.data}
@@ -103,23 +148,39 @@
}, },
//Computed properties //Computed properties
computed: { computed: {
stats() {
return this.metrics?.rendered.user ?? null
},
sponsors() {
return this.metrics?.rendered.plugins?.sponsors ?? null
},
ranked() { ranked() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? [] return this.metrics?.rendered.plugins?.achievements?.list?.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
}, },
achievements() { achievements() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? [] return this.metrics?.rendered.plugins?.achievements?.list?.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? []
}, },
introduction() { introduction() {
return this.metrics?.rendered.plugins.introduction?.text ?? "" return this.metrics?.rendered.plugins?.introduction?.text ?? ""
}, },
followup() { followup() {
return this.metrics?.rendered.plugins.followup ?? null return this.metrics?.rendered.plugins?.followup ?? null
}, },
habits() { calendar() {
return this.metrics?.rendered.plugins.habits.commits.hours ?? null if (this.metrics?.rendered.plugins?.calendar)
return Object.assign(this.metrics?.rendered.plugins?.calendar, {color(c) {
return {
"#ebedf0":"var(--color-calendar-graph-day-bg)",
"#9be9a8":"var(--color-calendar-graph-day-L1-bg)",
"#40c463":"var(--color-calendar-graph-day-L2-bg)",
"#30a14e":"var(--color-calendar-graph-day-L3-bg)",
"#216e39":"var(--color-calendar-graph-day-L4-bg)",
}[c] ?? c
}})
return null
}, },
isocalendar() { isocalendar() {
return (this.metrics?.rendered.plugins.isocalendar.svg ?? "") return (this.metrics?.rendered.plugins?.isocalendar?.svg ?? "")
.replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)") .replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
.replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)") .replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
.replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)") .replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
@@ -127,19 +188,31 @@
.replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)") .replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
}, },
languages() { languages() {
return this.metrics?.rendered.plugins.languages.favorites ?? [] return Object.assign(this.metrics?.rendered.plugins?.languages?.favorites ?? [], {total:this.metrics?.rendered.plugins?.languages.total})
},
reactions() {
return this.metrics?.rendered.plugins?.reactions ?? null
},
repositories() {
return this.metrics?.rendered.plugins?.repositories?.list ?? []
},
stars() {
return {repositories:this.metrics?.rendered.plugins?.stars?.repositories.map(({node, starredAt}) => ({...node, starredAt})) ?? []}
},
topics() {
return this.metrics?.rendered.plugins?.topics?.list ?? []
}, },
activity() { activity() {
return this.metrics?.rendered.plugins.activity.events ?? [] return this.metrics?.rendered.plugins?.activity?.events ?? []
}, },
contributions() { contributions() {
return this.metrics?.rendered.plugins.notable.contributions ?? [] return this.metrics?.rendered.plugins?.notable?.contributions ?? []
}, },
account() { account() {
if (!this.metrics) if (!this.metrics)
return null return null
const {login, name} = this.metrics.rendered.user const {login, name} = this.metrics?.rendered.user
return {login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account} return {login, name, avatar: this.metrics?.rendered.computed.avatar, type: this.metrics?.rendered.account}
}, },
url() { url() {
return `${window.location.protocol}//${window.location.host}/about/${this.user}` return `${window.location.protocol}//${window.location.host}/about/${this.user}`
@@ -166,6 +239,8 @@
pending: false, pending: false,
error: null, error: null,
config: {}, config: {},
progress: 0,
loaded: []
}, },
}) })
})() })()

View File

@@ -133,6 +133,29 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }
/* Topics */
.topics .list {
display: flex;
flex-wrap: wrap;
}
.topic {
display: flex;
align-items: center;
border-radius: 6px;
margin: .25rem;
padding: .25rem .5rem;
padding-left: .25rem;
color: var(--color-text-primary) !important;
border: 1px solid var(--color-border-primary);
}
.topic img {
height: 1.5rem;
width: 1.5rem;
margin-right: .5rem;
border-radius: 6px;
flex-shrink: 0;
}
/* Followup */ /* Followup */
.followup { .followup {
display: flex; display: flex;
@@ -144,10 +167,14 @@
margin-bottom: .5rem; margin-bottom: .5rem;
} }
/* Isocalendar */ /* Isocalendar and calendar */
.isocalendar .svg { .isocalendar .svg {
margin-top: 8rem; margin-top: 8rem;
} }
.calendar rect:hover {
cursor: pointer;
filter: brightness(4);
}
/* Activity */ /* Activity */
.activity > ul { .activity > ul {
@@ -198,13 +225,45 @@
color: var(--color-text-secondary); color: var(--color-text-secondary);
overflow-x: auto; overflow-x: auto;
} }
.activity quote p:first-child {
/* Markdown */
.markdown p:first-child {
margin-top: 0; margin-top: 0;
} }
.activity quote p:last-child { .markdown p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.activity img { .markdown p {
white-space: pre-wrap;
}
.markdown h1, .activity quote h2 {
border-bottom: 1px solid var(--color-markdown-frame-border);
}
.markdown blockquote {
margin: 0;
margin-bottom: 1rem;
padding: 0 .9rem;
border-left: .25rem solid var(--color-markdown-blockquote-border);
}
.markdown code {
background-color: var(--color-markdown-code-bg);
border-radius: .25rem;
padding: .125rem .25rem;
font-size: .9rem;
}
.markdown code[class^="language-"] {
display: block;
padding: 1rem;
}
.markdown ul {
margin-left: 0;
padding-left: 1rem;
font-size: 1rem !important;
}
.markdown input {
pointer-events: none
}
.markdown img {
max-width: 100%; max-width: 100%;
} }
@@ -231,6 +290,40 @@
font-size: 1.25rem; font-size: 1.25rem;
} }
/* Sponsors */
.goal {
background-color: var(--color-markdown-code-bg);
border-radius: .25rem;
display: block;
padding: .5rem 1rem;
margin-top: 1rem;
}
.goal p {
margin: 0 0 .5rem;
}
.goal .progress-wrap {
background-color: var(--color-markdown-table-tr-border);
height: 8px;
border-radius: 6px;
}
.goal .objective {
text-align: right;
font-style: italic;
font-size: .8rem;
}
.goal .sponsors-list {
display: flex;
flex-wrap: wrap;
}
img.avatar {
height: 2rem;
width: 2rem;
margin-right: .5rem;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 0 1px var(--color-avatar-border);
}
/* Ranked achievements */ /* Ranked achievements */
.rankeds { .rankeds {
margin-top: 2em; margin-top: 2em;
@@ -280,6 +373,79 @@
width: 100%; width: 100%;
} }
/* Reactions */
.reactions {
display: flex;
justify-content: center;
}
.reactions > div {
margin: 1.5rem;
}
.reactions .text {
text-align: center;
white-space: nowrap;
}
/* Repository */
.repositories {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.repository {
display: flex;
flex-direction: column;
width: 100%;
margin: .5rem;
max-width: 416px;
border-radius: .25rem;
border: 1px solid var(--color-border-primary);
padding: .5rem .5rem 0;
}
.repository .field {
display: flex;
align-items: center;
margin-bottom: .5rem;
}
.repository svg {
fill: currentColor;
}
.repository .infos, .repository .description {
font-size: .8rem;
}
.repository .name {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: .25rem;
flex-grow: 1;
}
.repository .name span:first-child {
color: var(--color-text-link);
}
.repository .name span:last-child {
color: var(--color-text-secondary);
font-size: .7rem;
}
.repository .infos > div {
color: var(--color-text-secondary);
display: flex;
align-items: center;
margin-right: 1rem;
}
.repository .infos svg {
margin: 0;
margin-right: .25rem;
}
/* Charts */ /* Charts */
.chart-bars { .chart-bars {
display: flex; display: flex;
@@ -306,12 +472,51 @@
width: 100%; width: 100%;
} }
/* Code highlighting */
.token.comment {
color: #669900;
}
.token.punctuation {
color: #8a93a8;
}
.token.namespace, .token.constant, .token.symbol, .token.keyword {
color: #b44418;
}
.token.regex, .token.string, .token.char, .token.number, .token.boolean {
color: #2777AA;
}
.token.property, .token.tag {
color: #48428a;
}
.token.builtin, .token.operator {
color: #106cbc;
}
.token.trimmed {
font-style: italic;
color: #77777760;
}
.token.coord {
color: #D2A8FF;
font-weight: bold;
}
.token.inserted:not(.prefix) {
color: #AAD0B4DC;
background-color: #336543DC;
}
.token.deleted:not(.prefix) {
color: #EED2D0DC;
background-color: #9A5256DC;
}
/* Icon gauges */ /* Icon gauges */
.gauge { .gauge {
stroke-linecap: round; stroke-linecap: round;
fill: none; fill: none;
color: #58A6FF; color: #58A6FF;
} }
.gauge text {
fill: #ffffff;
}
.gauge-base, .gauge-arc { .gauge-base, .gauge-arc {
stroke: currentColor; stroke: currentColor;
stroke-width: 6; stroke-width: 6;
@@ -438,6 +643,14 @@
outline: none; outline: none;
} }
/* */
small.footnote {
font-style: italic;
opacity: .8;
display: block;
text-align: left;
}
/* Media screen */ /* Media screen */
@media only screen and (min-width: 740px) { @media only screen and (min-width: 740px) {
.rankeds { .rankeds {
@@ -458,3 +671,18 @@
width: 520px; width: 520px;
} }
} }
/* Loading bar */
.loading-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 2px;
z-index: 9999;
}
.loading-bar > div {
background-color: #58A6FF;
height: 100%;
transition: all .12s ease-out;
}

View File

@@ -4,7 +4,7 @@
*/ */
//Setup //Setup
export default async function({login, graphql, rest, data, q, queries, imports}, conf) { export default async function({login, graphql, rest, data, q, queries, imports, callbacks}, conf) {
//Load inputs //Load inputs
console.debug(`metrics/compute/${login}/base > started`) console.debug(`metrics/compute/${login}/base > started`)
let {indepth, hireable, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"}) let {indepth, hireable, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
@@ -15,8 +15,10 @@ export default async function({login, graphql, rest, data, q, queries, imports},
console.debug(`metrics/compute/${login}/base > affiliations constraints ${affiliations}`) console.debug(`metrics/compute/${login}/base > affiliations constraints ${affiliations}`)
//Skip initial data gathering if not needed //Skip initial data gathering if not needed
if (conf.settings.notoken) if (conf.settings.notoken) {
await callbacks?.plugin?.(login, "base", true, data).catch(error => console.debug(`metrics/compute/${login}/plugins/callbacks > base > ${error}`))
return (postprocess.skip({login, data, imports}), {}) return (postprocess.skip({login, data, imports}), {})
}
//Base parts (legacy handling for web instance) //Base parts (legacy handling for web instance)
const defaulted = ("base" in q) ? legacy.converter(q.base) ?? true : true const defaulted = ("base" in q) ? legacy.converter(q.base) ?? true : true
@@ -194,6 +196,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`) console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`)
//Success //Success
console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`) console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`)
await callbacks?.plugin?.(login, "base", true, data).catch(error => console.debug(`metrics/compute/${login}/plugins/callbacks > base > ${error}`))
return {} return {}
} }
catch (error) { catch (error) {
@@ -208,6 +211,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
} }
//Not found //Not found
console.debug(`metrics/compute/${login}/base > no more account type`) console.debug(`metrics/compute/${login}/base > no more account type`)
await callbacks?.plugin?.(login, "base", false, data).catch(error => console.debug(`metrics/compute/${login}/plugins/callbacks > base > ${error}`))
throw new Error("user not found") throw new Error("user not found")
} }

View File

@@ -29,6 +29,14 @@
token: ${{ secrets.METRICS_TOKEN }} token: ${{ secrets.METRICS_TOKEN }}
config_output: png config_output: png
- name: Metrics insights
if: ${{ success() || failure() }}
uses: lowlighter/metrics@latest
with:
filename: metrics.insights.html
token: ${{ secrets.METRICS_TOKEN }}
config_output: insights
- name: Presets - name: Presets
uses: lowlighter/metrics@latest uses: lowlighter/metrics@latest
with: with:

View File

@@ -4,7 +4,7 @@
*/ */
//Setup //Setup
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) { export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template, callbacks}, {pending, imports}) {
//Load inputs //Load inputs
const {"config.animations": animations, "config.display": display, "config.timezone": _timezone, "config.base64": _base64, "debug.flags": dflags} = imports.metadata.plugins.core.inputs({data, account, q}) const {"config.animations": animations, "config.display": display, "config.timezone": _timezone, "config.base64": _base64, "debug.flags": dflags} = imports.metadata.plugins.core.inputs({data, account, q})
imports.metadata.templates[template].check({q, account, format: convert}) imports.metadata.templates[template].check({q, account, format: convert})
@@ -70,6 +70,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
finally { finally {
const result = {name, result: data.plugins[name]} const result = {name, result: data.plugins[name]}
console.debug(imports.util.inspect(result, {depth: Infinity, maxStringLength: 256, getters: true})) console.debug(imports.util.inspect(result, {depth: Infinity, maxStringLength: 256, getters: true}))
await callbacks?.plugin?.(login, name, !data.plugins[name].error, data.plugins[name]).catch(error => console.debug(`metrics/compute/${login}/plugins/callbacks > ${name} > ${error}`))
return result return result
} }
})()) })())

View File

@@ -52,8 +52,11 @@ export default async function({login, q, imports, data, graphql, queries, accoun
for (const [key, value] of Object.entries(list)) for (const [key, value] of Object.entries(list))
list[key] = {value, percentage: value / reactions.length, score: value / (display === "relative" ? max : reactions.length)} list[key] = {value, percentage: value / reactions.length, score: value / (display === "relative" ? max : reactions.length)}
//Compute total reactions
const total = Object.values(list).map(({value}) => value).reduce((a, b) => a + b, 0)
//Results //Results
return {list, comments: comments.length, details, days, twemoji: q["config.twemoji"]} return {list, total, comments: comments.length, details, days, twemoji: q["config.twemoji"]}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {

View File

@@ -32,6 +32,7 @@ export default async function({login, data, imports, q, account}, {enabled = fal
name: li.querySelector(".f3").innerText, name: li.querySelector(".f3").innerText,
description: li.querySelector(".f5").innerText, description: li.querySelector(".f5").innerText,
icon: li.querySelector("img")?.src ?? null, icon: li.querySelector("img")?.src ?? null,
url: li.querySelector("a")?.href ?? null,
})) }))
) )
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`) console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)