Refactor source paths
This commit is contained in:
270
source/app/action/index.mjs
Normal file
270
source/app/action/index.mjs
Normal file
@@ -0,0 +1,270 @@
|
||||
//Imports
|
||||
import core from "@actions/core"
|
||||
import github from "@actions/github"
|
||||
import octokit from "@octokit/graphql"
|
||||
import setup from "../setup.mjs"
|
||||
import mocks from "../mocks.mjs"
|
||||
import metrics from "../metrics.mjs"
|
||||
|
||||
;((async function () {
|
||||
//Yaml boolean converter
|
||||
const bool = (value, defaulted = false) => typeof value === "string" ? /^(?:[Tt]rue|[Oo]n|[Yy]es)$/.test(value) : defaulted
|
||||
//Debug message buffer
|
||||
const debugged = []
|
||||
//Runner
|
||||
try {
|
||||
//Initialization
|
||||
console.log(`GitHub metrics`)
|
||||
console.log("─".repeat(64))
|
||||
process.on("unhandledRejection", error => { throw error })
|
||||
|
||||
//Skip process if needed
|
||||
if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
|
||||
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
|
||||
console.log(`Skipped because [Skip GitHub Action] is in commit message`)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
//Load configuration
|
||||
const {conf, Plugins, Templates} = await setup({log:false})
|
||||
console.log(`Configuration │ loaded`)
|
||||
console.log(`Version │ ${conf.package.version}`)
|
||||
|
||||
//Debug mode
|
||||
const debug = bool(core.getInput("debug"))
|
||||
if (!debug)
|
||||
console.debug = message => debugged.push(message)
|
||||
console.log(`Debug mode │ ${debug}`)
|
||||
const dflags = (core.getInput("debug_flags") || "").split(" ").filter(flag => flag)
|
||||
console.log(`Debug flags │ ${dflags.join(" ") || "(none)"}`)
|
||||
|
||||
//Load svg template, style, fonts and query
|
||||
const template = core.getInput("template") || "classic"
|
||||
console.log(`Template to use │ ${template}`)
|
||||
|
||||
//Token for data gathering
|
||||
const token = core.getInput("token") || ""
|
||||
console.log(`Github token │ ${/^MOCKED/.test(token) ? "(MOCKED)" : token ? "provided" : "missing"}`)
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub token to gather your metrics")
|
||||
const api = {}
|
||||
api.graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
|
||||
console.log(`Github GraphQL API │ ok`)
|
||||
api.rest = github.getOctokit(token)
|
||||
console.log(`Github REST API │ ok`)
|
||||
//Apply mocking if needed
|
||||
if (bool(core.getInput("use_mocked_data"))) {
|
||||
Object.assign(api, await mocks(api))
|
||||
console.log(`Mocked Github API │ ok`)
|
||||
}
|
||||
//Extract octokits
|
||||
const {graphql, rest} = api
|
||||
|
||||
//SVG output
|
||||
const filename = core.getInput("filename") || "github-metrics.svg"
|
||||
console.log(`SVG output file │ ${filename}`)
|
||||
|
||||
//SVG optimization
|
||||
const optimize = bool(core.getInput("optimize"), true)
|
||||
conf.optimize = optimize
|
||||
console.log(`SVG optimization │ ${optimize}`)
|
||||
|
||||
//GitHub user
|
||||
let authenticated
|
||||
try {
|
||||
authenticated = (await rest.users.getAuthenticated()).data.login
|
||||
}
|
||||
catch {
|
||||
authenticated = github.context.repo.owner
|
||||
}
|
||||
const user = core.getInput("user") || authenticated
|
||||
console.log(`GitHub user │ ${user}`)
|
||||
|
||||
//Base elements
|
||||
const base = {}
|
||||
let parts = (core.getInput("base") || "").split(",").map(part => part.trim())
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
base[`base.${part}`] = parts.includes(part)
|
||||
console.log(`Base parts │ ${parts.join(", ") || "(none)"}`)
|
||||
|
||||
//Config
|
||||
const config = {
|
||||
"config.timezone":core.getInput("config_timezone") || ""
|
||||
}
|
||||
console.log(`Timezone │ ${config["config.timezone"] || "(system default)"}`)
|
||||
|
||||
//Additional plugins
|
||||
const plugins = {
|
||||
lines:{enabled:bool(core.getInput("plugin_lines"))},
|
||||
traffic:{enabled:bool(core.getInput("plugin_traffic"))},
|
||||
pagespeed:{enabled:bool(core.getInput("plugin_pagespeed"))},
|
||||
habits:{enabled:bool(core.getInput("plugin_habits"))},
|
||||
languages:{enabled:bool(core.getInput("plugin_languages"))},
|
||||
followup:{enabled:bool(core.getInput("plugin_followup"))},
|
||||
music:{enabled:bool(core.getInput("plugin_music"))},
|
||||
posts:{enabled:bool(core.getInput("plugin_posts"))},
|
||||
isocalendar:{enabled:bool(core.getInput("plugin_isocalendar"))},
|
||||
gists:{enabled:bool(core.getInput("plugin_gists"))},
|
||||
topics:{enabled:bool(core.getInput("plugin_topics"))},
|
||||
projects:{enabled:bool(core.getInput("plugin_projects"))},
|
||||
tweets:{enabled:bool(core.getInput("plugin_tweets"))},
|
||||
}
|
||||
let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
|
||||
console.log(`Plugins enabled │ ${Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).join(", ")}`)
|
||||
//Additional plugins options
|
||||
//Pagespeed
|
||||
if (plugins.pagespeed.enabled) {
|
||||
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || ""
|
||||
q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`))
|
||||
q[`pagespeed.screenshot`] = bool(core.getInput(`plugin_pagespeed_screenshot`))
|
||||
console.log(`Pagespeed token │ ${/^MOCKED/.test(plugins.pagespeed.token) ? "(MOCKED)" : plugins.pagespeed.token ? "provided" : "missing"}`)
|
||||
console.log(`Pagespeed detailed │ ${q["pagespeed.detailed"]}`)
|
||||
console.log(`Pagespeed screenshot │ ${q["pagespeed.screenshot"]}`)
|
||||
}
|
||||
//Languages
|
||||
if (plugins.languages.enabled) {
|
||||
for (const option of ["ignored", "skipped"])
|
||||
q[`languages.${option}`] = core.getInput(`plugin_languages_${option}`) || null
|
||||
console.log(`Languages ignored │ ${q["languages.ignored"] || "(none)"}`)
|
||||
console.log(`Languages skipped repos │ ${q["languages.skipped"] || "(none)"}`)
|
||||
}
|
||||
//Habits
|
||||
if (plugins.habits.enabled) {
|
||||
for (const option of ["from", "days"])
|
||||
q[`habits.${option}`] = core.getInput(`plugin_habits_${option}`) || null
|
||||
q[`habits.facts`] = bool(core.getInput(`plugin_habits_facts`))
|
||||
q[`habits.charts`] = bool(core.getInput(`plugin_habits_charts`))
|
||||
console.log(`Habits facts │ ${q["habits.facts"]}`)
|
||||
console.log(`Habits charts │ ${q["habits.charts"]}`)
|
||||
console.log(`Habits events to use │ ${q["habits.from"] || "(default)"}`)
|
||||
console.log(`Habits days to keep │ ${q["habits.days"] || "(default)"}`)
|
||||
}
|
||||
//Music
|
||||
if (plugins.music.enabled) {
|
||||
plugins.music.token = core.getInput("plugin_music_token") || ""
|
||||
for (const option of ["provider", "mode", "playlist", "limit"])
|
||||
q[`music.${option}`] = core.getInput(`plugin_music_${option}`) || null
|
||||
console.log(`Music provider │ ${q["music.provider"] || "(none)"}`)
|
||||
console.log(`Music plugin mode │ ${q["music.mode"] || "(none)"}`)
|
||||
console.log(`Music playlist │ ${q["music.playlist"] || "(none)"}`)
|
||||
console.log(`Music tracks limit │ ${q["music.limit"] || "(default)"}`)
|
||||
console.log(`Music token │ ${/^MOCKED/.test(plugins.music.token) ? "(MOCKED)" : plugins.music.token ? "provided" : "missing"}`)
|
||||
}
|
||||
//Posts
|
||||
if (plugins.posts.enabled) {
|
||||
for (const option of ["source", "limit"])
|
||||
q[`posts.${option}`] = core.getInput(`plugin_posts_${option}`) || null
|
||||
console.log(`Posts source │ ${q["posts.source"] || "(none)"}`)
|
||||
console.log(`Posts limit │ ${q["posts.limit"] || "(default)"}`)
|
||||
}
|
||||
//Isocalendar
|
||||
if (plugins.isocalendar.enabled) {
|
||||
q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") || "half-year"
|
||||
console.log(`Isocalendar duration │ ${q["isocalendar.duration"]}`)
|
||||
}
|
||||
//Topics
|
||||
if (plugins.topics.enabled) {
|
||||
for (const option of ["mode", "sort", "limit"])
|
||||
q[`topics.${option}`] = core.getInput(`plugin_topics_${option}`) || null
|
||||
console.log(`Topics mode │ ${q["topics.mode"] || "(default)"}`)
|
||||
console.log(`Topics sort mode │ ${q["topics.sort"] || "(default)"}`)
|
||||
console.log(`Topics limit │ ${q["topics.limit"] || "(default)"}`)
|
||||
}
|
||||
//Projects
|
||||
if (plugins.projects.enabled) {
|
||||
for (const option of ["limit", "repositories"])
|
||||
q[`projects.${option}`] = core.getInput(`plugin_projects_${option}`) || null
|
||||
console.log(`Projects limit │ ${q["projects.limit"] || "(default)"}`)
|
||||
console.log(`Projects repositories │ ${q["projects.repositories"] || "(none)"}`)
|
||||
}
|
||||
//Tweets
|
||||
if (plugins.tweets.enabled) {
|
||||
plugins.tweets.token = core.getInput("plugin_tweets_token") || null
|
||||
for (const option of ["limit"])
|
||||
q[`tweets.${option}`] = core.getInput(`plugin_tweets_${option}`) || null
|
||||
console.log(`Twitter token │ ${/^MOCKED/.test(plugins.tweets.token) ? "(MOCKED)" : plugins.tweets.token ? "provided" : "missing"}`)
|
||||
console.log(`Tweets limit │ ${q["tweets.limit"] || "(default)"}`)
|
||||
}
|
||||
|
||||
//Repositories to use
|
||||
const repositories = Number(core.getInput("repositories")) || 100
|
||||
console.log(`Repositories to use │ ${repositories}`)
|
||||
|
||||
//Die on plugins errors
|
||||
const die = bool(core.getInput("plugins_errors_fatal"))
|
||||
console.log(`Plugin errors │ ${die ? "die" : "warn"}`)
|
||||
|
||||
//Verify svg
|
||||
const verify = bool(core.getInput("verify"))
|
||||
console.log(`Verify SVG │ ${verify}`)
|
||||
|
||||
//Build query
|
||||
const query = JSON.parse(core.getInput("query") || "{}")
|
||||
console.log(`Query additional params │ ${JSON.stringify(query)}`)
|
||||
q = {...query, ...q, base:false, ...base, ...config, repositories, template}
|
||||
|
||||
//Render metrics
|
||||
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
|
||||
console.log(`Render │ complete`)
|
||||
|
||||
//Commit to repository
|
||||
const dryrun = bool(core.getInput("dryrun"))
|
||||
if (dryrun)
|
||||
console.log(`Dry-run │ complete`)
|
||||
else {
|
||||
//Repository and branch
|
||||
const branch = github.context.ref.replace(/^refs[/]heads[/]/, "")
|
||||
console.log(`Repository │ ${github.context.repo.owner}/${github.context.repo.repo}`)
|
||||
console.log(`Branch │ ${branch}`)
|
||||
//Committer token
|
||||
const token = core.getInput("committer_token") || core.getInput("token") || ""
|
||||
console.log(`Committer token │ ${/^MOCKED/.test(token) ? "(MOCKED)" : token ? "provided" : "missing"}`)
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub token to commit your metrics")
|
||||
const rest = github.getOctokit(token)
|
||||
console.log(`Committer REST API │ ok`)
|
||||
try {
|
||||
console.log(`Committer │ ${(await rest.users.getAuthenticated()).data.login}`)
|
||||
}
|
||||
catch {
|
||||
console.log(`Committer │ (github-actions)`)
|
||||
}
|
||||
//Retrieve previous render SHA to be able to update file content through API
|
||||
let sha = null
|
||||
try {
|
||||
const {repository:{object:{oid}}} = await graphql(`
|
||||
query Sha {
|
||||
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
|
||||
object(expression: "${branch}:${filename}") { ... on Blob { oid } }
|
||||
}
|
||||
}
|
||||
`
|
||||
)
|
||||
sha = oid
|
||||
} catch (error) { console.debug(error) }
|
||||
console.log(`Previous render sha │ ${sha ?? "(none)"}`)
|
||||
//Update file content through API
|
||||
await rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
|
||||
content:Buffer.from(rendered).toString("base64"),
|
||||
...(sha ? {sha} : {})
|
||||
})
|
||||
console.log(`Commit to repo │ ok`)
|
||||
}
|
||||
|
||||
//Success
|
||||
console.log(`Success, thanks for using metrics !`)
|
||||
process.exit(0)
|
||||
|
||||
}
|
||||
//Errors
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
if (!bool(core.getInput("debug")))
|
||||
for (const log of ["─".repeat(64), "An error occured, logging debug message :", ...debugged])
|
||||
console.log(log)
|
||||
core.setFailed(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
})()).catch(error => process.exit(1))
|
||||
227
source/app/metrics.mjs
Normal file
227
source/app/metrics.mjs
Normal file
@@ -0,0 +1,227 @@
|
||||
//Imports
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import paths from "path"
|
||||
import util from "util"
|
||||
import axios from "axios"
|
||||
import url from "url"
|
||||
import puppeteer from "puppeteer"
|
||||
import processes from "child_process"
|
||||
import ejs from "ejs"
|
||||
import imgb64 from "image-to-base64"
|
||||
import SVGO from "svgo"
|
||||
|
||||
//Setup
|
||||
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false}, {Plugins, Templates}) {
|
||||
//Compute rendering
|
||||
try {
|
||||
|
||||
//Init
|
||||
console.debug(`metrics/compute/${login} > start`)
|
||||
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
|
||||
const template = q.template || conf.settings.templates.default
|
||||
const repositories = Math.max(0, Number(q.repositories)) || conf.settings.repositories || 100
|
||||
const pending = []
|
||||
const s = (value, end = "") => value > 1 ? {y:"ies", "":"s"}[end] : end
|
||||
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
|
||||
throw new Error("unsupported template")
|
||||
const {image, style, fonts} = conf.templates[template]
|
||||
const queries = conf.queries
|
||||
const data = {base:{}, config:{}, errors:[], plugins:{}, computed:{}}
|
||||
|
||||
//Base parts
|
||||
{
|
||||
const defaulted = ("base" in q) ? !!q.base : true
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
data.base[part] = `base.${part}` in q ? !!q[ `base.${part}`] : defaulted
|
||||
}
|
||||
|
||||
//Placeholder
|
||||
if (login === "placeholder")
|
||||
placeholder({data, conf, q})
|
||||
//Compute
|
||||
else {
|
||||
//Query data from GitHub API
|
||||
console.debug(`metrics/compute/${login} > graphql query`)
|
||||
Object.assign(data, await graphql(queries.common({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString()})))
|
||||
//Query repositories from GitHub API
|
||||
{
|
||||
//Iterate through repositories
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login} > retrieving repositories after ${cursor}`)
|
||||
const {user:{repositories:{edges, nodes}}} = await graphql(queries.repositories({login, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, 100)}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
data.user.repositories.nodes.push(...nodes)
|
||||
pushed = nodes.length
|
||||
} while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login} > keeping only ${repositories} repositories`)
|
||||
data.user.repositories.nodes.splice(repositories)
|
||||
console.debug(`metrics/compute/${login} > loaded ${data.user.repositories.nodes.length} repositories`)
|
||||
}
|
||||
//Compute metrics
|
||||
console.debug(`metrics/compute/${login} > compute`)
|
||||
const computer = Templates[template].default || Templates[template]
|
||||
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
|
||||
const promised = await Promise.all(pending)
|
||||
|
||||
//Check plugins errors
|
||||
{
|
||||
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
|
||||
if (errors.length) {
|
||||
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
|
||||
if (die)
|
||||
throw new Error(`An error occured during rendering, dying`)
|
||||
else
|
||||
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Template rendering
|
||||
console.debug(`metrics/compute/${login} > render`)
|
||||
let rendered = await ejs.render(image, {...data, s, style, fonts}, {async:true})
|
||||
|
||||
//Optimize rendering
|
||||
if ((conf.optimize)&&(!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
|
||||
const {data:optimized} = await svgo.optimize(rendered)
|
||||
rendered = optimized
|
||||
}
|
||||
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
const libxmljs = (await import("libxmljs")).default
|
||||
const parsed = libxmljs.parseXml(rendered)
|
||||
if (parsed.errors.length)
|
||||
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
|
||||
}
|
||||
|
||||
//Result
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
return rendered
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//User not found
|
||||
if (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")))
|
||||
throw new Error("user not found")
|
||||
//Generic error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** Formatter */
|
||||
function format(n) {
|
||||
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
|
||||
if (n/v >= 1)
|
||||
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
|
||||
return n
|
||||
}
|
||||
|
||||
/** Bytes formatter */
|
||||
function bytes(n) {
|
||||
for (const {u, v} of [{u:"E", v:10**18}, {u:"P", v:10**15}, {u:"T", v:10**12}, {u:"G", v:10**9}, {u:"M", v:10**6}, {u:"k", v:10**3}])
|
||||
if (n/v >= 1)
|
||||
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
|
||||
return `${n} byte${n > 1 ? "s" : ""}`
|
||||
}
|
||||
|
||||
/** Array shuffler */
|
||||
function shuffle(array) {
|
||||
for (let i = array.length-1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random()*(i+1))
|
||||
;[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
/** Escape html */
|
||||
function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
|
||||
return string
|
||||
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&" : "&")
|
||||
.replace(/</g, u["<"] ? "<" : "<")
|
||||
.replace(/>/g, u[">"] ? ">" : ">")
|
||||
.replace(/"/g, u['"'] ? """ : '"')
|
||||
.replace(/'/g, u["'"] ? "'" : "'")
|
||||
}
|
||||
|
||||
/** Expand url */
|
||||
async function urlexpand(url) {
|
||||
try {
|
||||
return (await axios.get(url)).request.res.responseUrl
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/** Run command */
|
||||
async function run(command, options) {
|
||||
return await new Promise((solve, reject) => {
|
||||
console.debug(`metrics/command > ${command}`)
|
||||
const child = processes.exec(command, options)
|
||||
let [stdout, stderr] = ["", ""]
|
||||
child.stdout.on("data", data => stdout += data)
|
||||
child.stderr.on("data", data => stderr += data)
|
||||
child.on("close", code => {
|
||||
console.debug(`metrics/command > ${command} > exited with code ${code}`)
|
||||
return code === 0 ? solve(stdout) : reject(stderr)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Placeholder generator */
|
||||
function placeholder({data, conf, q}) {
|
||||
//Proxifier
|
||||
const proxify = (target) => (typeof target === "object")&&(target) ? new Proxy(target, {
|
||||
get(target, property) {
|
||||
//Primitive conversion
|
||||
if (property === Symbol.toPrimitive)
|
||||
return () => "##"
|
||||
//Iterables
|
||||
if (property === Symbol.iterator)
|
||||
return Reflect.get(target, property)
|
||||
//Plugins should not be proxified by default as they can be toggled by user
|
||||
if (/^plugins$/.test(property))
|
||||
return Reflect.get(target, property)
|
||||
//Consider no errors on plugins
|
||||
if (/^error/.test(property))
|
||||
return undefined
|
||||
//Proxify recursively
|
||||
return proxify(property in target ? Reflect.get(target, property) : {})
|
||||
}
|
||||
}) : target
|
||||
//Enabled plugins
|
||||
const enabled = Object.entries(conf.settings.plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).filter(key => (key in q)&&(q[key]))
|
||||
//Placeholder data
|
||||
Object.assign(data, {
|
||||
s(_, letter) { return letter === "y" ? "ies" : "s" },
|
||||
meta:{version:conf.package.version, author:conf.package.author, placeholder:true},
|
||||
user:proxify({name:`############`, websiteUrl:`########################`, isHireable:false}),
|
||||
computed:proxify({
|
||||
avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
|
||||
registration:"## years ago",
|
||||
cakeday:false,
|
||||
calendar:new Array(14).fill({color:"#ebedf0"}),
|
||||
licenses:{favorite:`########`},
|
||||
token:{scopes:[]},
|
||||
}),
|
||||
plugins:Object.fromEntries(enabled.map(key =>
|
||||
[key, proxify({
|
||||
posts:{source:"########", list:new Array("posts.limit" in q ? Math.max(Number(q["posts.limit"])||0, 0) : 2).fill({title:"###### ###### ####### ######", date:"####"})},
|
||||
music:{provider:"########", tracks:new Array("music.limit" in q ? Math.max(Number(q["music.limit"])||0, 0) : 4).fill({name:"##########", artist:"######", artwork:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})},
|
||||
pagespeed:{detailed:!!q["pagespeed.detailed"], screenshot:!!q["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null, scores:["Performance", "Accessibility", "Best Practices", "SEO"].map(title => ({title, score:NaN}))},
|
||||
followup:{issues:{count:0}, pr:{count:0}},
|
||||
habits:{facts:!!(q["habits.facts"] ?? 1), charts:!!q["habits.charts"], indents:{style:`########`}, commits:{day:"####"}, linguist:{ordered:[]}},
|
||||
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
|
||||
topics:{mode:"topics.mode" in q ? q["topics.mode"] : "starred", list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
|
||||
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
|
||||
tweets:{profile:{username:"########", verified:false}, list:[...new Array("tweets.limit" in q ? Math.max(Number(q["tweets.limit"])||0, 0) : 2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]},
|
||||
}[key]??{})]
|
||||
)),
|
||||
})
|
||||
}
|
||||
765
source/app/mocks.mjs
Normal file
765
source/app/mocks.mjs
Normal file
@@ -0,0 +1,765 @@
|
||||
//Imports
|
||||
import axios from "axios"
|
||||
import urls from "url"
|
||||
|
||||
//Mocked state
|
||||
let mocked = false
|
||||
|
||||
//Mocking
|
||||
export default async function ({graphql, rest}) {
|
||||
|
||||
//Check if already mocked
|
||||
if (mocked)
|
||||
return {graphql, rest}
|
||||
mocked = true
|
||||
console.debug(`metrics/compute/mocks > mocking`)
|
||||
|
||||
//GraphQL API mocking
|
||||
{
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api`)
|
||||
const unmocked = graphql
|
||||
graphql = new Proxy(unmocked, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [query] = args
|
||||
//Common query
|
||||
if (/^query Metrics /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Metrics`)
|
||||
return ({
|
||||
user: {
|
||||
databaseId:22963968,
|
||||
name:"Simon Lecoq",
|
||||
login:"lowlighter",
|
||||
createdAt:"2016-10-20T16:49:29Z",
|
||||
avatarUrl:"https://avatars0.githubusercontent.com/u/22963968?u=f5097de6f06ed2e31906f784163fc1e9fc84ed57&v=4",
|
||||
websiteUrl:"https://simon.lecoq.io",
|
||||
isHireable:false,
|
||||
twitterUsername:"lecoqsimon",
|
||||
repositories:{totalCount:Math.floor(Math.random()*100), totalDiskUsage:Math.floor(Math.random()*100000), nodes:[]},
|
||||
packages:{totalCount:Math.floor(Math.random()*10)},
|
||||
starredRepositories:{totalCount:Math.floor(Math.random()*1000)},
|
||||
watching:{totalCount:Math.floor(Math.random()*100)},
|
||||
sponsorshipsAsSponsor:{totalCount:Math.floor(Math.random()*5)},
|
||||
sponsorshipsAsMaintainer:{totalCount:Math.floor(Math.random()*5)},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:Math.floor(Math.random()*30),
|
||||
totalCommitContributions:Math.floor(Math.random()*1000),
|
||||
restrictedContributionsCount:Math.floor(Math.random()*500),
|
||||
totalIssueContributions:Math.floor(Math.random()*100),
|
||||
totalPullRequestContributions:Math.floor(Math.random()*100),
|
||||
totalPullRequestReviewContributions:Math.floor(Math.random()*100)
|
||||
},
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks:[
|
||||
{
|
||||
contributionDays:[
|
||||
{color:"#40c463"},
|
||||
{color:"#ebedf0"},
|
||||
{color:"#9be9a8"},
|
||||
{color:"#ebedf0"},
|
||||
{color:"#ebedf0"}
|
||||
]
|
||||
},
|
||||
{
|
||||
contributionDays:[
|
||||
{color:"#30a14e"},
|
||||
{color:"#9be9a8"},
|
||||
{color:"#40c463"},
|
||||
{color:"#9be9a8"},
|
||||
{color:"#ebedf0"},
|
||||
{color:"#ebedf0"},
|
||||
{color:"#ebedf0"}
|
||||
]
|
||||
},
|
||||
{
|
||||
contributionDays:[
|
||||
{color:"#40c463"},
|
||||
{color:"#216e39"},
|
||||
{color:"#9be9a8"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
repositoriesContributedTo:{totalCount:Math.floor(Math.random()*10)},
|
||||
followers:{totalCount:Math.floor(Math.random()*100)},
|
||||
following:{totalCount:Math.floor(Math.random()*100)},
|
||||
issueComments:{totalCount:Math.floor(Math.random()*100)},
|
||||
organizations:{totalCount:Math.floor(Math.random()*5)}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Repositories query
|
||||
if (/^query Repositories /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Repositories`)
|
||||
return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({
|
||||
user:{
|
||||
repositories:{
|
||||
edges:[],
|
||||
nodes:[],
|
||||
}
|
||||
}
|
||||
}) : ({
|
||||
user:{
|
||||
repositories:{
|
||||
edges:[
|
||||
{
|
||||
cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
],
|
||||
nodes:[
|
||||
{
|
||||
name:"metrics",
|
||||
watchers:{totalCount:Math.floor(Math.random()*100)},
|
||||
stargazers:{totalCount:Math.floor(Math.random()*1000)},
|
||||
languages:{
|
||||
edges:[
|
||||
{size:111733, node:{color:"#f1e05a", name:"JavaScript"}
|
||||
},
|
||||
{size:14398, node:{color:"#563d7c", name:"CSS"}},
|
||||
{size:13223, node:{color:"#e34c26", name:"HTML"}},
|
||||
]
|
||||
},
|
||||
issues_open:{totalCount:Math.floor(Math.random()*100)},
|
||||
issues_closed:{totalCount:Math.floor(Math.random()*100)},
|
||||
pr_open:{totalCount:Math.floor(Math.random()*100)},
|
||||
pr_merged:{totalCount:Math.floor(Math.random()*100)},
|
||||
releases:{totalCount:Math.floor(Math.random()*100)},
|
||||
forkCount:Math.floor(Math.random()*100),
|
||||
licenseInfo:{spdxId:"MIT"}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Single repository query
|
||||
if (/^query Repository /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Repository`)
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
name:"metrics",
|
||||
createdAt:new Date().toISOString(),
|
||||
diskUsage:Math.floor(Math.random()*10000),
|
||||
watchers:{totalCount:Math.floor(Math.random()*100)},
|
||||
stargazers:{totalCount:Math.floor(Math.random()*1000)},
|
||||
languages:{
|
||||
edges:[
|
||||
{size:111733, node:{color:"#f1e05a", name:"JavaScript"}
|
||||
},
|
||||
{size:14398, node:{color:"#563d7c", name:"CSS"}},
|
||||
{size:13223, node:{color:"#e34c26", name:"HTML"}},
|
||||
]
|
||||
},
|
||||
issues_open:{totalCount:Math.floor(Math.random()*100)},
|
||||
issues_closed:{totalCount:Math.floor(Math.random()*100)},
|
||||
pr_open:{totalCount:Math.floor(Math.random()*100)},
|
||||
pr_merged:{totalCount:Math.floor(Math.random()*100)},
|
||||
releases:{totalCount:Math.floor(Math.random()*100)},
|
||||
forkCount:Math.floor(Math.random()*100),
|
||||
licenseInfo:{spdxId:"MIT"}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
//Calendar query
|
||||
if (/^query Calendar /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Calendar`)
|
||||
//Generate calendar
|
||||
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const weeks = []
|
||||
let contributionDays = []
|
||||
for (; date <= to; date.setDate(date.getDate()+1)) {
|
||||
//Create new week on sunday
|
||||
if (date.getDay() === 0) {
|
||||
weeks.push({contributionDays})
|
||||
contributionDays = []
|
||||
}
|
||||
//Random contributions
|
||||
const contributionCount = Math.min(10, Math.max(0, Math.floor(Math.random()*14-4)))
|
||||
contributionDays.push({
|
||||
contributionCount,
|
||||
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)],
|
||||
date:date.toISOString().substring(0, 10)
|
||||
})
|
||||
}
|
||||
return ({
|
||||
user: {
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Gists query
|
||||
if (/^query Gists /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Projects`)
|
||||
return ({
|
||||
user:{
|
||||
gists:{
|
||||
totalCount:1,
|
||||
nodes:[
|
||||
{
|
||||
stargazerCount:Math.floor(Math.random()*10),
|
||||
isFork:false,
|
||||
forks:{totalCount:Math.floor(Math.random()*10)},
|
||||
files:[{name:"example"}],
|
||||
comments:{totalCount:Math.floor(Math.random()*10)}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Projects query
|
||||
if (/^query Projects /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > Projects`)
|
||||
return ({
|
||||
user:{
|
||||
projects:{
|
||||
totalCount:1,
|
||||
nodes:[
|
||||
{
|
||||
name:"User-owned project",
|
||||
updatedAt:new Date().toISOString(),
|
||||
progress:{
|
||||
doneCount:Math.floor(Math.random()*10),
|
||||
inProgressCount:Math.floor(Math.random()*10),
|
||||
todoCount:Math.floor(Math.random()*10),
|
||||
enabled:true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Repository project query
|
||||
if (/^query RepositoryProject /.test(query)) {
|
||||
console.debug(`metrics/compute/mocks > mocking graphql api result > RepositoryProject`)
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
project:{
|
||||
name:"Repository project example",
|
||||
updatedAt:new Date().toISOString(),
|
||||
progress:{
|
||||
doneCount:Math.floor(Math.random()*10),
|
||||
inProgressCount:Math.floor(Math.random()*10),
|
||||
todoCount:Math.floor(Math.random()*10),
|
||||
enabled:true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Rest API mocking
|
||||
{
|
||||
console.debug(`metrics/compute/mocks > mocking rest api`)
|
||||
const unmocked = {
|
||||
request:rest.request,
|
||||
rateLimit:rest.rateLimit.get,
|
||||
listEventsForAuthenticatedUser:rest.activity.listEventsForAuthenticatedUser,
|
||||
getViews:rest.repos.getViews,
|
||||
getContributorsStats:rest.repos.getContributorsStats,
|
||||
listCommits:rest.repos.listCommits,
|
||||
}
|
||||
|
||||
//Raw request
|
||||
rest.request = new Proxy(unmocked.request, {
|
||||
apply:function(target, that, args) {
|
||||
//Arguments
|
||||
const [url] = args
|
||||
//Head request
|
||||
if (/^HEAD .$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request HEAD`)
|
||||
return ({
|
||||
status: 200,
|
||||
url:"https://api.github.com/",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:undefined
|
||||
})
|
||||
}
|
||||
//Commit content
|
||||
if (/^https:..api.github.com.repos.lowlighter.metrics.commits.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
|
||||
return ({
|
||||
status: 200,
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
data:{
|
||||
sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
commit:{
|
||||
author:{
|
||||
name:"lowlighter",
|
||||
email:"22963968+lowlighter@users.noreply.github.com",
|
||||
date:new Date().toISOString(),
|
||||
},
|
||||
committer:{
|
||||
name:"lowlighter",
|
||||
email:"22963968+lowlighter@users.noreply.github.com",
|
||||
date:new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
author:{
|
||||
login:"lowlighter",
|
||||
id:22963968,
|
||||
},
|
||||
committer:{
|
||||
login:"lowlighter",
|
||||
id:22963968,
|
||||
},
|
||||
files: [
|
||||
{
|
||||
sha:"5ab8c4fb6a0be4c157419c3b9d7b522dca354b3f",
|
||||
filename:"index.mjs",
|
||||
patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file"
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return target(...args)
|
||||
}
|
||||
})
|
||||
|
||||
//Rate limit
|
||||
rest.rateLimit.get = new Proxy(unmocked.rateLimit, {
|
||||
apply:function(target, that, args) {
|
||||
return ({
|
||||
status: 200,
|
||||
url:"https://api.github.com/rate_limit",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
resources:{
|
||||
core:{limit:5000, used:0, remaining:5000, reset:0 },
|
||||
search:{limit:30, used:0, remaining:30, reset:0 },
|
||||
graphql:{limit:5000, used:0, remaining:5000, reset:0 },
|
||||
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0 },
|
||||
source_import:{limit:100, used:0, remaining:100, reset:0 },
|
||||
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0 },
|
||||
},
|
||||
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
//Events list
|
||||
rest.activity.listEventsForAuthenticatedUser = new Proxy(unmocked.listEventsForAuthenticatedUser, {
|
||||
apply:function(target, that, [{page, per_page}]) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser`)
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/users/lowlighter/events?per_page=${per_page}&page=${page}`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:page < 1 ? new Array(10).fill(null).map(() =>
|
||||
(false ? {
|
||||
id:"10000000001",
|
||||
type:"IssueCommentEvent",
|
||||
} : {
|
||||
id:"10000000000",
|
||||
type:"PushEvent",
|
||||
actor:{
|
||||
id:22963968,
|
||||
login:"lowlighter",
|
||||
},
|
||||
repo: {
|
||||
id:293860197,
|
||||
name:"lowlighter/metrics",
|
||||
},
|
||||
payload: {
|
||||
ref:"refs/heads/master",
|
||||
commits: [
|
||||
{
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
}
|
||||
]
|
||||
},
|
||||
created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString()
|
||||
})
|
||||
) : []
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
//Repository traffic
|
||||
rest.repos.getViews = new Proxy(unmocked.getViews, {
|
||||
apply:function(target, that, args) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getViews`)
|
||||
const count = Math.floor(Math.random()*1000)*2
|
||||
const uniques = Math.floor(Math.random()*count)*2
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/traffic/views",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
count,
|
||||
uniques,
|
||||
views:[
|
||||
{timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2},
|
||||
{timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2},
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
//Repository contributions
|
||||
rest.repos.getContributorsStats = new Proxy(unmocked.getContributorsStats, {
|
||||
apply:function(target, that, args) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats`)
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/stats/contributors",
|
||||
headers: {
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:[
|
||||
{
|
||||
total:Math.floor(Math.random()*1000),
|
||||
weeks:[
|
||||
{w:1, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
|
||||
{w:2, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
|
||||
{w:3, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
|
||||
{w:4, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
|
||||
],
|
||||
author: {
|
||||
login:"lowlighter",
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
//Repository contributions
|
||||
rest.repos.listCommits = new Proxy(unmocked.listCommits, {
|
||||
apply:function(target, that, [{page, per_page}]) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listCommits`)
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/lowlighter/metrics/commits?per_page=${per_page}&page=${page}`,
|
||||
headers: {
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:page < 2 ? new Array(per_page).fill(null).map(() =>
|
||||
({
|
||||
sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
commit:{
|
||||
author:{
|
||||
name:"lowlighter",
|
||||
date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString()
|
||||
},
|
||||
committer:{
|
||||
name:"lowlighter",
|
||||
date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString()
|
||||
},
|
||||
}
|
||||
})
|
||||
) : []
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Axios mocking
|
||||
{
|
||||
console.debug(`metrics/compute/mocks > mocking axios`)
|
||||
const unmocked = {
|
||||
get:axios.get,
|
||||
post:axios.post,
|
||||
}
|
||||
|
||||
//Post requests
|
||||
axios.post = new Proxy(unmocked.post, {
|
||||
apply:function(target, that, args) {
|
||||
//Arguments
|
||||
const [url, body] = args
|
||||
//Spotify api
|
||||
if (/^https:..accounts.spotify.com.api.token/.test(url)) {
|
||||
//Access token generator
|
||||
const params = new urls.URLSearchParams(body)
|
||||
if ((params.get("grant_type") === "refresh_token")&&(params.get("client_id") === "MOCKED_CLIENT_ID")&&(params.get("client_secret") === "MOCKED_CLIENT_SECRET")&&(params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
access_token:"MOCKED_TOKEN_ACCESS",
|
||||
token_type:"Bearer",
|
||||
expires_in:3600,
|
||||
scope:"user-read-recently-played user-read-private",
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return target(...args)
|
||||
}
|
||||
})
|
||||
|
||||
//Get requests
|
||||
axios.get = new Proxy(unmocked.get, {
|
||||
apply:function(target, that, args) {
|
||||
//Arguments
|
||||
const [url, options] = args
|
||||
//Pagespeed api
|
||||
if (/^https:..www.googleapis.com.pagespeedonline.v5/.test(url)) {
|
||||
//Pagespeed result
|
||||
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
captchaResult:"CAPTCHA_NOT_NEEDED",
|
||||
id:"https://simon.lecoq.io/",
|
||||
lighthouseResult:{
|
||||
requestedUrl:"https://simon.lecoq.io/",
|
||||
finalUrl:"https://simon.lecoq.io/",
|
||||
lighthouseVersion:"6.3.0",
|
||||
audits:{
|
||||
"final-screenshot":{
|
||||
id:"final-screenshot",
|
||||
title:"Final Screenshot",
|
||||
score: null,
|
||||
details:{
|
||||
data:"data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
|
||||
type:"screenshot",
|
||||
timestamp:Date.now()
|
||||
}
|
||||
},
|
||||
metrics:{
|
||||
id:"metrics",
|
||||
title:"Metrics",
|
||||
score: null,
|
||||
details:{
|
||||
items:[
|
||||
{
|
||||
observedFirstContentfulPaint:283,
|
||||
observedFirstVisualChangeTs:1789259909429,
|
||||
observedFirstContentfulPaintTs:1789259857628,
|
||||
firstContentfulPaint:370,
|
||||
observedDomContentLoaded:251,
|
||||
observedFirstMeaningfulPaint:642,
|
||||
maxPotentialFID:203,
|
||||
observedLoad:330,
|
||||
firstMeaningfulPaint:370,
|
||||
observedCumulativeLayoutShift:0.0028944855967078186,
|
||||
observedSpeedIndex:711,
|
||||
observedSpeedIndexTs:1789260285891,
|
||||
observedTimeOriginTs:1789259574429,
|
||||
observedLargestContentfulPaint:857,
|
||||
cumulativeLayoutShift:0.0028944855967078186,
|
||||
observedFirstPaintTs:1789259857628,
|
||||
observedTraceEndTs:1789261300953,
|
||||
largestContentfulPaint:1085,
|
||||
observedTimeOrigin:0,
|
||||
speedIndex:578,
|
||||
observedTraceEnd:1727,
|
||||
observedDomContentLoadedTs:1789259825567,
|
||||
observedFirstPaint:283,
|
||||
totalBlockingTime:133,
|
||||
observedLastVisualChangeTs:1789260426429,
|
||||
observedFirstVisualChange:335,
|
||||
observedLargestContentfulPaintTs:1789260431554,
|
||||
estimatedInputLatency:13,
|
||||
observedLoadTs:1789259904916,
|
||||
observedLastVisualChange:852,
|
||||
firstCPUIdle:773,
|
||||
interactive:953,
|
||||
observedNavigationStartTs:1789259574429,
|
||||
observedNavigationStart:0,
|
||||
observedFirstMeaningfulPaintTs:1789260216895
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
},
|
||||
categories:{
|
||||
"best-practices":{
|
||||
id:"best-practices",
|
||||
title:"Best Practices",
|
||||
score:Math.floor(Math.random()*100)/100,
|
||||
},
|
||||
seo:{
|
||||
id:"seo",
|
||||
title:"SEO",
|
||||
score:Math.floor(Math.random()*100)/100,
|
||||
},
|
||||
accessibility:{
|
||||
id:"accessibility",
|
||||
title:"Accessibility",
|
||||
score:Math.floor(Math.random()*100)/100,
|
||||
},
|
||||
performance: {
|
||||
id:"performance",
|
||||
title:"Performance",
|
||||
score:Math.floor(Math.random()*100)/100,
|
||||
}
|
||||
},
|
||||
},
|
||||
analysisUTCTimestamp:new Date().toISOString()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
//Spotify api
|
||||
if (/^https:..api.spotify.com/.test(url)) {
|
||||
//Get recently played tracks
|
||||
if (/me.player.recently-played/.test(url)&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
track:{
|
||||
album:{
|
||||
album_type:"single",
|
||||
artists:[
|
||||
{
|
||||
name:"EGOIST",
|
||||
type:"artist",
|
||||
}
|
||||
],
|
||||
images:[
|
||||
{
|
||||
height:640,
|
||||
url:"https://i.scdn.co/image/ab67616d0000b27366371d0ad05c3f402d9cb2ae",
|
||||
width:640
|
||||
},
|
||||
{
|
||||
height:300,
|
||||
url:"https://i.scdn.co/image/ab67616d00001e0266371d0ad05c3f402d9cb2ae",
|
||||
width:300
|
||||
},
|
||||
{
|
||||
height:64,
|
||||
url:"https://i.scdn.co/image/ab67616d0000485166371d0ad05c3f402d9cb2ae",
|
||||
width:64
|
||||
}
|
||||
],
|
||||
name:"Fallen",
|
||||
release_date:"2014-11-19",
|
||||
type:"album",
|
||||
},
|
||||
artists:[
|
||||
{
|
||||
name:"EGOIST",
|
||||
type:"artist",
|
||||
}
|
||||
],
|
||||
name:"Fallen",
|
||||
preview_url:"https://p.scdn.co/mp3-preview/f30eb6d1c55afa13ce754559a41ab683a1a76b02?cid=fa6ae353840041ee8af3bd1d21a66783",
|
||||
type:"track",
|
||||
},
|
||||
played_at:new Date().toISOString(),
|
||||
context:{
|
||||
type:"album",
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
//Twitter api
|
||||
if (/^https:..api.twitter.com/.test(url)) {
|
||||
//Get user profile
|
||||
if ((/users.by.username/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
profile_image_url:"https://pbs.twimg.com/profile_images/1338344493234286592/C_ujKIUa_normal.png",
|
||||
name:"GitHub",
|
||||
verified:true,
|
||||
id:"13334762",
|
||||
username:"github",
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
//Get recent tweets
|
||||
if ((/tweets.search.recent/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:[
|
||||
{
|
||||
id:"1000000000000000001",
|
||||
created_at:new Date().toISOString(),
|
||||
entities:{
|
||||
mentions:[
|
||||
{start:22, end:33, username:"lowlighter"},
|
||||
],
|
||||
},
|
||||
text:"Checkout metrics from @lowlighter ! #GitHub",
|
||||
},
|
||||
{
|
||||
id:"1000000000000000000",
|
||||
created_at:new Date().toISOString(),
|
||||
text:"Hello world !",
|
||||
}
|
||||
],
|
||||
includes:{
|
||||
users:[
|
||||
{
|
||||
id:"100000000000000000",
|
||||
name:"lowlighter",
|
||||
username:"lowlighter",
|
||||
},
|
||||
]
|
||||
},
|
||||
meta:{
|
||||
newest_id:"1000000000000000001",
|
||||
oldest_id:"1000000000000000000",
|
||||
result_count:2,
|
||||
next_token:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return target(...args)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//Return mocked elements
|
||||
return {graphql, rest}
|
||||
}
|
||||
|
||||
119
source/app/setup.mjs
Normal file
119
source/app/setup.mjs
Normal file
@@ -0,0 +1,119 @@
|
||||
//Imports
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import util from "util"
|
||||
import url from "url"
|
||||
const Templates = {}
|
||||
const Plugins = {}
|
||||
|
||||
/** Setup */
|
||||
export default async function ({log = true} = {}) {
|
||||
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../..")
|
||||
const __statics = path.join(__metrics, "source/app/web/statics")
|
||||
const __templates = path.join(__metrics, "source/templates")
|
||||
const __plugins = path.join(__metrics, "source/plugins")
|
||||
const __queries = path.join(__metrics, "source/queries")
|
||||
const __package = path.join(__metrics, "package.json")
|
||||
const __settings = path.join(__metrics, "settings.json")
|
||||
const __modules = path.join(__metrics, "node_modules")
|
||||
|
||||
//Init
|
||||
const logger = log ? console.debug : () => null
|
||||
logger(`metrics/setup > setup`)
|
||||
const conf = {
|
||||
templates:{},
|
||||
queries:{},
|
||||
settings:{},
|
||||
statics:__statics,
|
||||
node_modules:__modules,
|
||||
}
|
||||
|
||||
//Load settings
|
||||
logger(`metrics/setup > load settings.json`)
|
||||
if (fs.existsSync(__settings)) {
|
||||
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
|
||||
logger(`metrics/setup > load settings.json > success`)
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > load settings.json > (missing)`)
|
||||
if (!conf.settings.templates)
|
||||
conf.settings.templates = {default:"classic", enabled:[]}
|
||||
if (!conf.settings.plugins)
|
||||
conf.settings.plugins = {}
|
||||
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
|
||||
if (conf.settings.debug)
|
||||
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load package settings
|
||||
logger(`metrics/setup > load package.json`)
|
||||
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
|
||||
logger(`metrics/setup > load package.json > success`)
|
||||
|
||||
//Load templates
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
//Cache templates file
|
||||
if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
|
||||
continue
|
||||
logger(`metrics/setup > load template [${name}]`)
|
||||
const files = ["image.svg", "style.css", "fonts.css"].map(file => path.join(__templates, (fs.existsSync(path.join(__templates, name, file)) ? name : "classic"), file))
|
||||
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
|
||||
conf.templates[name] = {image, style, fonts}
|
||||
//Cache templates scripts
|
||||
Templates[name] = (await import(url.pathToFileURL(path.join(__templates, name, "template.mjs")).href)).default
|
||||
logger(`metrics/setup > load template [${name}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(conf.templates, name, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload template [${name}]`)
|
||||
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
|
||||
logger(`metrics/setup > reload template [${name}] > success`)
|
||||
return {image, style, fonts}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Load plugins
|
||||
for (const name of await fs.promises.readdir(__plugins)) {
|
||||
//Cache plugins scripts
|
||||
logger(`metrics/setup > load plugin [${name}]`)
|
||||
Plugins[name] = (await import(url.pathToFileURL(path.join(__plugins, name, "index.mjs")).href)).default
|
||||
logger(`metrics/setup > load plugin [${name}] > success`)
|
||||
}
|
||||
|
||||
//Load queries
|
||||
for (const query of await fs.promises.readdir(__queries)) {
|
||||
//Cache queries
|
||||
const name = query.replace(/[.]graphql$/, "")
|
||||
logger(`metrics/setup > load query [${name}]`)
|
||||
conf.queries[`_${name}`] = `${await fs.promises.readFile(path.join(__queries, query))}`
|
||||
logger(`metrics/setup > load query [${name}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(conf.queries, `_${name}`, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload query [${name}]`)
|
||||
const raw = `${fs.readFileSync(path.join(__queries, query))}`
|
||||
logger(`metrics/setup > reload query [${name}] > success`)
|
||||
return raw
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Create queries formatters
|
||||
Object.keys(conf.queries).map(name => conf.queries[name.substring(1)] = (vars = {}) => {
|
||||
let query = conf.queries[name]
|
||||
for (const [key, value] of Object.entries(vars))
|
||||
query = query.replace(new RegExp(`[$]${key}`, "g"), value)
|
||||
return query
|
||||
})
|
||||
|
||||
//Conf
|
||||
logger(`metrics/setup > setup > success`)
|
||||
return {Templates, Plugins, conf}
|
||||
|
||||
}
|
||||
5
source/app/web/index.mjs
Normal file
5
source/app/web/index.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
//Imports
|
||||
import app from "./instance.mjs"
|
||||
|
||||
//Start app
|
||||
await app({mock:process.env.USE_MOCKED_DATA})
|
||||
189
source/app/web/instance.mjs
Normal file
189
source/app/web/instance.mjs
Normal file
@@ -0,0 +1,189 @@
|
||||
//Imports
|
||||
import octokit from "@octokit/graphql"
|
||||
import OctokitRest from "@octokit/rest"
|
||||
import express from "express"
|
||||
import ratelimit from "express-rate-limit"
|
||||
import compression from "compression"
|
||||
import cache from "memory-cache"
|
||||
import util from "util"
|
||||
import setup from "../setup.mjs"
|
||||
import mocks from "../mocks.mjs"
|
||||
import metrics from "../metrics.mjs"
|
||||
|
||||
/** App */
|
||||
export default async function ({mock = false} = {}) {
|
||||
|
||||
//Load configuration settings
|
||||
const {conf, Plugins, Templates} = await setup()
|
||||
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
|
||||
|
||||
//Apply configuration mocking if needed
|
||||
if (mock) {
|
||||
console.debug(`metrics/app > using mocked settings`)
|
||||
const {settings} = conf
|
||||
//Mock token if it's undefined
|
||||
if (!settings.token)
|
||||
settings.token = (console.debug(`metrics/app > using mocked token`), "MOCKED_TOKEN")
|
||||
//Mock plugins state and tokens if they're undefined
|
||||
for (const plugin of Object.keys(Plugins)) {
|
||||
if (!settings.plugins[plugin])
|
||||
settings.plugins[plugin] = {}
|
||||
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > using mocked token enable state for ${plugin}`), true)
|
||||
if (["tweets", "pagespeed"].includes(plugin))
|
||||
settings.plugins[plugin].token = settings.plugins[plugin].token ?? (console.debug(`metrics/app > using mocked token for ${plugin}`), "MOCKED_TOKEN")
|
||||
if (["music"].includes(plugin))
|
||||
settings.plugins[plugin].token = settings.plugins[plugin].token ?? (console.debug(`metrics/app > using mocked token for ${plugin}`), "MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN")
|
||||
}
|
||||
console.debug(util.inspect(settings, {depth:Infinity, maxStringLength:256}))
|
||||
}
|
||||
|
||||
//Load octokits
|
||||
const api = {graphql:octokit.graphql.defaults({headers:{authorization: `token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
|
||||
//Apply mocking if needed
|
||||
if (mock)
|
||||
Object.assign(api, await mocks(api))
|
||||
const {graphql, rest} = api
|
||||
|
||||
//Setup server
|
||||
const app = express()
|
||||
app.use(compression())
|
||||
const middlewares = []
|
||||
//Rate limiter middleware
|
||||
if (ratelimiter) {
|
||||
app.set("trust proxy", 1)
|
||||
middlewares.push(ratelimit({
|
||||
skip(req, res) { return !!cache.get(req.params.login) },
|
||||
message:"Too many requests",
|
||||
...ratelimiter
|
||||
}))
|
||||
}
|
||||
//Cache headers middleware
|
||||
middlewares.push((req, res, next) => {
|
||||
if (!["/placeholder"].includes(req.path))
|
||||
res.header("Cache-Control", cached ? `public, max-age=${cached}` : "no-store, no-cache")
|
||||
next()
|
||||
})
|
||||
|
||||
//Base routes
|
||||
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 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)
|
||||
const actions = {flush:new Map()}
|
||||
app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`))
|
||||
app.get("/favicon.ico", limiter, (req, res) => res.sendStatus(204))
|
||||
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
||||
app.get("/.requests", limiter, async (req, res) => res.status(200).json((await rest.rateLimit.get()).data.rate))
|
||||
app.get("/.templates", limiter, (req, res) => res.status(200).json(templates))
|
||||
app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled))
|
||||
app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts))
|
||||
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.statics}/style.css`))
|
||||
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/themes/prism-tomorrow.css`))
|
||||
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.statics}/app.js`))
|
||||
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/ejs/ejs.min.js`))
|
||||
app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.js`))
|
||||
app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.node_modules}/axios/dist/axios.min.map`))
|
||||
app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue/dist/vue.min.js`))
|
||||
app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`))
|
||||
app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`))
|
||||
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/prism.js`))
|
||||
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/components/prism-yaml.min.js`))
|
||||
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.node_modules}/prismjs/components/prism-markdown.min.js`))
|
||||
app.get("/.uncache", limiter, async (req, res) => {
|
||||
const {token, user} = req.query
|
||||
if (token) {
|
||||
if (actions.flush.has(token)) {
|
||||
console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`)
|
||||
cache.del(actions.flush.get(token))
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
else
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
else {
|
||||
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
||||
actions.flush.set(token, user)
|
||||
return res.json({token})
|
||||
}
|
||||
})
|
||||
|
||||
//Metrics
|
||||
app.get("/:login", ...middlewares, async (req, res) => {
|
||||
|
||||
//Request params
|
||||
const {login} = req.params
|
||||
if ((restricted.length)&&(!restricted.includes(login))) {
|
||||
console.debug(`metrics/app/${login} > 403 (not in whitelisted users)`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
//Read cached data if possible
|
||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.send(cache.get(login))
|
||||
return
|
||||
}
|
||||
//Maximum simultaneous users
|
||||
if ((maxusers)&&(cache.size()+1 > maxusers)) {
|
||||
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
||||
return res.sendStatus(503)
|
||||
}
|
||||
|
||||
//Compute rendering
|
||||
try {
|
||||
//Render
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`)
|
||||
const q = parse(req.query)
|
||||
const rendered = await metrics({login, q}, {graphql, rest, plugins, conf, die:q["plugins.errors.fatal"] ?? false, verify:q["verify"] ?? false}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)&&(login !== "placeholder"))
|
||||
cache.put(login, rendered, cached)
|
||||
//Send response
|
||||
res.header("Content-Type", "image/svg+xml")
|
||||
res.send(rendered)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 404 (user not found)`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
//Invalid template
|
||||
if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 400 (bad request)`)
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
//General error
|
||||
console.error(error)
|
||||
res.sendStatus(500)
|
||||
}
|
||||
})
|
||||
|
||||
//Listen
|
||||
app.listen(port, () => console.log([
|
||||
`Listening on port │ ${port}`,
|
||||
`Debug mode │ ${debug}`,
|
||||
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`Cached time │ ${cached} seconds`,
|
||||
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
|
||||
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
|
||||
`Plugins enabled │ ${enabled.join(", ")}`,
|
||||
`Server ready !`
|
||||
].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 === "true"
|
||||
//Parse null
|
||||
if (/^null$/.test(value))
|
||||
query[key] = null
|
||||
}
|
||||
return query
|
||||
}
|
||||
197
source/app/web/statics/app.js
Normal file
197
source/app/web/statics/app.js
Normal file
@@ -0,0 +1,197 @@
|
||||
;(async function() {
|
||||
//Init
|
||||
const url = new URLSearchParams(window.location.search)
|
||||
const {data:templates} = await axios.get("/.templates")
|
||||
const {data:plugins} = await axios.get("/.plugins")
|
||||
const {data:base} = await axios.get("/.plugins.base")
|
||||
const {data:version} = await axios.get("/.version")
|
||||
//App
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el:"main",
|
||||
async mounted() {
|
||||
//Load instance
|
||||
await this.load()
|
||||
//Interpolate config from browser
|
||||
try {
|
||||
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
} catch (error) {}
|
||||
},
|
||||
components:{Prism:PrismComponent},
|
||||
//Data initialization
|
||||
data:{
|
||||
version,
|
||||
user:url.get("user") || "",
|
||||
palette:url.get("palette") || (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") || "light",
|
||||
requests:{limit:0, used:0, remaining:0, reset:0},
|
||||
config:{
|
||||
timezone:"",
|
||||
},
|
||||
plugins:{
|
||||
base,
|
||||
list:plugins,
|
||||
enabled:{base:Object.fromEntries(base.map(key => [key, true]))},
|
||||
descriptions:{
|
||||
pagespeed:"Website performances",
|
||||
languages:"Most used languages",
|
||||
followup:"Issues and pull requests",
|
||||
traffic:"Pages views",
|
||||
lines:"Lines of code changed",
|
||||
habits:"Coding habits",
|
||||
music:"Music plugin",
|
||||
posts:"Recent posts",
|
||||
isocalendar:"Isometric commit calendar",
|
||||
gists:"Gists metrics",
|
||||
topics:"Starred topics",
|
||||
projects:"Projects",
|
||||
tweets:"Latest tweets",
|
||||
"base.header":"Header",
|
||||
"base.activity":"Account activity",
|
||||
"base.community":"Community stats",
|
||||
"base.repositories":"Repositories metrics",
|
||||
"base.metadata":"Metadata",
|
||||
},
|
||||
options:{
|
||||
"languages.ignored":"",
|
||||
"languages.skipped":"",
|
||||
"pagespeed.detailed":false,
|
||||
"pagespeed.screenshot":false,
|
||||
"habits.from":200,
|
||||
"habits.days":14,
|
||||
"habits.facts":true,
|
||||
"habits.charts":false,
|
||||
"music.playlist":"",
|
||||
"music.limit":4,
|
||||
"posts.limit":4,
|
||||
"posts.source":"dev.to",
|
||||
"isocalendar.duration":"half-year",
|
||||
"projects.limit":4,
|
||||
"projects.repositories":"",
|
||||
"topics.mode":"starred",
|
||||
"topics.sort":"stars",
|
||||
"topics.limit":12,
|
||||
"tweets.limit":2,
|
||||
},
|
||||
},
|
||||
templates:{
|
||||
list:templates,
|
||||
selected:url.get("template") || templates[0],
|
||||
loaded:{},
|
||||
placeholder:"",
|
||||
descriptions:{
|
||||
classic:"Classic template",
|
||||
terminal:"Terminal template",
|
||||
repository:"(hidden)",
|
||||
},
|
||||
},
|
||||
generated:{
|
||||
pending:false,
|
||||
content:"",
|
||||
error:false,
|
||||
},
|
||||
},
|
||||
//Computed data
|
||||
computed:{
|
||||
//User's repository
|
||||
repo() {
|
||||
return `https://github.com/${this.user}/${this.user}`
|
||||
},
|
||||
//Endpoint to use for computed metrics
|
||||
url() {
|
||||
//Plugins enabled
|
||||
const plugins = Object.entries(this.plugins.enabled)
|
||||
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
|
||||
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
|
||||
.map(([key, value]) => `${key}=${+value}`)
|
||||
//Plugins options
|
||||
const options = Object.entries(this.plugins.options)
|
||||
.filter(([key, value]) => `${value}`.length)
|
||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Config
|
||||
const config = Object.entries(this.config).filter(([key, value]) => value).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||
//Template
|
||||
const template = (this.templates.selected !== templates[0]) ? [`template=${this.templates.selected}`] : []
|
||||
//Generated url
|
||||
const params = [...template, ...plugins, ...options, ...config].join("&")
|
||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||
},
|
||||
//Embedded generated code
|
||||
embed() {
|
||||
return ``
|
||||
},
|
||||
//GitHub action auto-generated code
|
||||
action() {
|
||||
return [
|
||||
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
|
||||
`name: GitHub metrics`,
|
||||
`on:`,
|
||||
` # Schedule updates`,
|
||||
` schedule: [{cron: "0 * * * *"}]`,
|
||||
` push: {branches: "master"}`,
|
||||
`jobs:`,
|
||||
` github-metrics:`,
|
||||
` runs-on: ubuntu-latest`,
|
||||
` steps:`,
|
||||
` - uses: lowlighter/metrics@latest`,
|
||||
` with:`,
|
||||
` # You'll need to setup a personal token in your secrets.`,
|
||||
` token: ${"$"}{{ secrets.METRICS_TOKEN }}`,
|
||||
` # GITHUB_TOKEN is a special auto-generated token used for commits`,
|
||||
` committer_token: ${"$"}{{ secrets.GITHUB_TOKEN }}`,
|
||||
``,
|
||||
` # Options`,
|
||||
` user: ${this.user }`,
|
||||
` template: ${this.templates.selected}`,
|
||||
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ")||'""'}`,
|
||||
...[
|
||||
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base")&&(value)).map(([key]) => ` plugin_${key}: yes`),
|
||||
...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
||||
...Object.entries(this.config).filter(([key, value]) => value).map(([key, value]) => ` config_${key.replace(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
||||
].sort(),
|
||||
].join("\n")
|
||||
}
|
||||
},
|
||||
//Methods
|
||||
methods:{
|
||||
//Load and render image
|
||||
async load() {
|
||||
//Render placeholder
|
||||
const url = this.url.replace(new RegExp(`${this.user}(\\?|$)`), "placeholder$1")
|
||||
this.templates.placeholder = this.serialize((await axios.get(url)).data)
|
||||
this.generated.content = ""
|
||||
//Start GitHub rate limiter tracker
|
||||
this.ghlimit()
|
||||
},
|
||||
//Generate metrics and flush cache
|
||||
async generate() {
|
||||
//Avoid requests spamming
|
||||
if (this.generated.pending)
|
||||
return
|
||||
this.generated.pending = true
|
||||
//Compute metrics
|
||||
try {
|
||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
||||
this.generated.content = this.serialize((await axios.get(this.url)).data)
|
||||
} catch {
|
||||
this.generated.error = true
|
||||
}
|
||||
finally {
|
||||
this.generated.pending = false
|
||||
}
|
||||
this.ghlimit({once:true})
|
||||
},
|
||||
//Serialize svg
|
||||
serialize(svg) {
|
||||
return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svg)))}`
|
||||
},
|
||||
//Update reate limit requests
|
||||
async ghlimit({once = false} = {}) {
|
||||
const {data:requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
if (!once)
|
||||
setTimeout(() => this.ghlimit(), 30*1000)
|
||||
}
|
||||
},
|
||||
})
|
||||
})()
|
||||
246
source/app/web/statics/index.html
Normal file
246
source/app/web/statics/index.html
Normal file
@@ -0,0 +1,246 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>📊 GitHub metrics</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="A SVG image generator which includes activity, community and repositories metrics about your GitHub account that you can includes on your profile">
|
||||
<meta name="author" content="lowlighter">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="/.css/style.css">
|
||||
<link rel="stylesheet" href="/.css/style.prism.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Vue app -->
|
||||
<main :class="[palette]">
|
||||
<!-- Title -->
|
||||
<template>
|
||||
<h1><a href="https://github.com/lowlighter/metrics">Metrics v{{ version }}</a></h1>
|
||||
</template>
|
||||
<!-- Content -->
|
||||
<template>
|
||||
<section class="generator">
|
||||
<!-- Steps panel -->
|
||||
<section class="steps">
|
||||
<div class="step">
|
||||
<h2>1. Enter your GitHub username</h2>
|
||||
<input type="text" name="user" v-model="user" maxlength="39" placeholder="GitHub username" :disabled="generated.pending">
|
||||
</div>
|
||||
<div class="step">
|
||||
<h2>2. Select a template</h2>
|
||||
<div class="templates">
|
||||
<label v-for="template in templates.list" :key="template" v-show="templates.descriptions[template] !== '(hidden)'">
|
||||
<input type="radio" v-model="templates.selected" :value="template" @change="load" :disabled="generated.pending">
|
||||
{{ templates.descriptions[template] || template }}
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="plugins.base.length">
|
||||
<h3>2.1 Configure base content</h3>
|
||||
<div class="plugins">
|
||||
<label v-for="part in plugins.base" :key="part">
|
||||
<input type="checkbox" v-model="plugins.enabled.base[part]" @change="load" :disabled="generated.pending">
|
||||
{{ plugins.descriptions[`base.${part}`] || `base.${part}` }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="plugins.list.length">
|
||||
<h3>2.2 Enable additional plugins</h3>
|
||||
<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>
|
||||
<i>*Additional plugins may be available when used as GitHub Action</i>
|
||||
<template v-if="(plugins.enabled.tweets)||(plugins.enabled.music)||(plugins.enabled.pagespeed)||(plugins.enabled.languages)||(plugins.enabled.habits)||(plugins.enabled.posts)||(plugins.enabled.isocalendar)||(plugins.enabled.projects)||(plugins.enabled.topics)">
|
||||
<h3>2.3 Configure additional plugins</h3>
|
||||
<div class="options">
|
||||
<div class="options-group" v-if="plugins.enabled.tweets">
|
||||
<h4>{{ plugins.descriptions.tweets }}</h4>
|
||||
<label>
|
||||
Number of tweets to display
|
||||
<input type="number" v-model="plugins.options['tweets.limit']" min="1" max="10" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.music">
|
||||
<h4>{{ plugins.descriptions.music }}</h4>
|
||||
<label>
|
||||
Playlist embed link
|
||||
<input type="text" v-model="plugins.options['music.playlist']" placeholder="https://embed.music.apple.com/en/playlist/">
|
||||
</label>
|
||||
<label>
|
||||
Number of tracks to display
|
||||
<input type="number" v-model="plugins.options['music.limit']" min="1" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.pagespeed">
|
||||
<h4>{{ plugins.descriptions.pagespeed }}</h4>
|
||||
<label>
|
||||
Detailed PageSpeed report
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.detailed']" @change="load">
|
||||
</label>
|
||||
<label>
|
||||
Include a website screenshot
|
||||
<input type="checkbox" v-model="plugins.options['pagespeed.screenshot']" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.languages">
|
||||
<h4>{{ plugins.descriptions.languages }}</h4>
|
||||
<label>
|
||||
Ignored languages (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.ignored']" @change="load">
|
||||
</label>
|
||||
<label>
|
||||
Skipped repositories (comma separated)
|
||||
<input type="text" v-model="plugins.options['languages.skipped']" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.habits">
|
||||
<h4>{{ plugins.descriptions.habits }}</h4>
|
||||
<label>
|
||||
Number of events for habits
|
||||
<input type="number" v-model="plugins.options['habits.from']" min="1" max="1000">
|
||||
</label>
|
||||
<label>
|
||||
Number of days for habits
|
||||
<input type="number" v-model="plugins.options['habits.days']" min="1" max="30">
|
||||
</label>
|
||||
<label>
|
||||
Display tidbits
|
||||
<input type="checkbox" v-model="plugins.options['habits.facts']" @change="load">
|
||||
</label>
|
||||
<label>
|
||||
Display activity charts
|
||||
<input type="checkbox" v-model="plugins.options['habits.charts']" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.posts">
|
||||
<h4>{{ plugins.descriptions.posts }}</h4>
|
||||
<label>
|
||||
Posts source
|
||||
<select v-model="plugins.options['posts.source']" disabled>
|
||||
<option value="dev.to">dev.to</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Number of posts to display
|
||||
<input type="number" v-model="plugins.options['posts.limit']" min="1" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.isocalendar">
|
||||
<h4>{{ plugins.descriptions.isocalendar }}</h4>
|
||||
<label>
|
||||
Isocalendar duration
|
||||
<select v-model="plugins.options['isocalendar.duration']">
|
||||
<option value="half-year">Half year</option>
|
||||
<option value="full-year">Full year</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.topics">
|
||||
<h4>{{ plugins.descriptions.topics }}</h4>
|
||||
<label>
|
||||
Topics display mode
|
||||
<select v-model="plugins.options['topics.mode']" @change="load">
|
||||
<option value="starred">Starred topics</option>
|
||||
<option value="mastered">Known and mastered technologies</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Topics sorting
|
||||
<select v-model="plugins.options['topics.sort']">
|
||||
<option value="starred">Recently starred by you</option>
|
||||
<option value="stars">Most stars</option>
|
||||
<option value="activity">Recent actity</option>
|
||||
<option value="random">Random</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Number of topics to display
|
||||
<input type="number" v-model="plugins.options['topics.limit']" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
<div class="options-group" v-if="plugins.enabled.projects">
|
||||
<h4>{{ plugins.descriptions.projects }}</h4>
|
||||
<label>
|
||||
Number of projects to display
|
||||
<input type="number" v-model="plugins.options['projects.limit']" min="1" max="100" @change="load">
|
||||
</label>
|
||||
<label>
|
||||
Repositories projects to display (comma separated)
|
||||
<input type="text" v-model="plugins.options['projects.repositories']" @change="load">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="step">
|
||||
<h2>3. Generate your metrics</h2>
|
||||
<template v-if="!user">
|
||||
Set your username to generate your metrics 🦑
|
||||
</template>
|
||||
<div class="preview-inliner">
|
||||
<template v-if="generated.content">
|
||||
<img class="metrics preview-inline" :src="generated.content" alt="metrics">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img class="metrics preview-inline" :src="templates.placeholder" alt="metrics">
|
||||
</template>
|
||||
<div class="error" v-if="generated.error">An error occurred. Please try again later.</div>
|
||||
</div>
|
||||
<template v-if="user">
|
||||
<button @click="generate" :disabled="generated.pending">{{ generated.pending ? "Working on it :)" : "Generate your metrics !" }}</button>
|
||||
</template>
|
||||
<div class="palette">
|
||||
Generated metrics use transparency and colors which can be read on both light and dark modes, so everyone can see your stats whatever their preferred color scheme !
|
||||
<div class="palettes">
|
||||
<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>4. Embed these metrics on your GitHub profile</h2>
|
||||
For even more features, be sure to checkout <a href="https://github.com/lowlighter/metrics">lowlighter/metrics</a> !
|
||||
<template v-if="user">
|
||||
<h3>4.1 Using <a href="#">{{ window.location.host }}</a></h3>
|
||||
Add the markdown below in your <i>README.md</i> at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code"><Prism language="markdown" :code="embed"></Prism></div>
|
||||
<h3>4. Using <a href="https://github.com/marketplace/actions/github-metrics-as-svg-image">GitHub action</a></h3>
|
||||
Create a new workflow with the following content at <a :href="repo">{{ user }}/{{ user }}</a>
|
||||
<div class="code"><Prism language="yaml" :code="action"></Prism></div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Metrics preview -->
|
||||
<section class="preview">
|
||||
<template v-if="generated.content">
|
||||
<img class="metrics" :src="generated.content" alt="metrics">
|
||||
</template>
|
||||
<template v-else>
|
||||
<img class="metrics" :src="templates.placeholder" alt="metrics">
|
||||
</template>
|
||||
<div class="error" v-if="generated.error">An error occurred. Please try again later.</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
<!-- GitHub requests tracker -->
|
||||
<template>
|
||||
<div class="gh-requests">{{ requests.remaining }} GitHub request{{ requests.remaining > 1 ? "s" : "" }} remaining</div>
|
||||
</template>
|
||||
</main>
|
||||
<!-- Scripts -->
|
||||
<script src="/.js/axios.min.js"></script>
|
||||
<script src="/.js/prism.min.js"></script>
|
||||
<script src="/.js/prism.markdown.min.js"></script>
|
||||
<script src="/.js/prism.yaml.min.js"></script>
|
||||
<script src="/.js/ejs.min.js"></script>
|
||||
<script src="/.js/vue.min.js"></script>
|
||||
<script src="/.js/vue.prism.min.js"></script>
|
||||
<script src="/.js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
213
source/app/web/statics/style.css
Normal file
213
source/app/web/statics/style.css
Normal file
@@ -0,0 +1,213 @@
|
||||
/* General */
|
||||
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;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 1rem 1.5rem;
|
||||
overflow-x: hidden;
|
||||
transition: background-color .3s;
|
||||
}
|
||||
/* Headlines */
|
||||
h1 {
|
||||
font-size: 1.6rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
h2 {
|
||||
margin: 1.5rem 0 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
h3 {
|
||||
margin: .5rem 0 .25rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
/* Links */
|
||||
a, a:hover, a:visited {
|
||||
color: #0366D6;
|
||||
text-decoration: none;
|
||||
font-style: normal;
|
||||
outline: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #79B8FF;
|
||||
transition: color .4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Inputs */
|
||||
input, button, select {
|
||||
border-radius: .5rem;
|
||||
padding: .25rem .5rem;
|
||||
outline: none;
|
||||
border: 1px solid #E1E4E8;
|
||||
background-color: #FAFBFC;
|
||||
color: #1B1F23;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
input[name=user] {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
input[type=text], select, button {
|
||||
min-width: 50%;
|
||||
}
|
||||
option {
|
||||
text-align: center;
|
||||
}
|
||||
label, button {
|
||||
margin: 1rem;
|
||||
}
|
||||
label {
|
||||
padding-right: .25rem;
|
||||
padding-bottom: .125rem;
|
||||
}
|
||||
input[disabled], button[disabled], select[disabled] {
|
||||
opacity: .5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
label:hover {
|
||||
border-radius: .25rem;
|
||||
background-color: #79B8FF50;
|
||||
transition: background-color .4s;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* Generator */
|
||||
.generator {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.generator .step {
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
.generator .steps {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.generator .preview {
|
||||
display: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.generator .preview .metrics {
|
||||
width: 480px;
|
||||
}
|
||||
.generator .preview-inliner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.generator .preview-inliner .metrics {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
@media only screen and (min-width: 1180px) {
|
||||
.generator .preview-inliner {
|
||||
display: none;
|
||||
}
|
||||
.generator .preview {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
/* Plugins */
|
||||
.plugins, .palettes {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.plugins label, .palettes label {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.options-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.options-group label {
|
||||
margin: 0;
|
||||
}
|
||||
.options-group h4 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
/* Code snippets */
|
||||
.code {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
.code pre {
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
.code .language-markdown {
|
||||
word-break: break-all !important;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
details {
|
||||
width: 100%;
|
||||
}
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
/* Color palette */
|
||||
.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;
|
||||
}
|
||||
/* Error */
|
||||
.error {
|
||||
color: #721c24;
|
||||
background-color: #f8d7da;
|
||||
padding: .75rem 1.25rem;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: .25rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
/* Github requests */
|
||||
.gh-requests {
|
||||
position: fixed;
|
||||
right: .25rem;
|
||||
bottom: .25rem;
|
||||
font-size: .8rem;
|
||||
}
|
||||
28
source/plugins/followup/index.mjs
Normal file
28
source/plugins/followup/index.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
//Setup
|
||||
export default async function ({computed, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.followup))
|
||||
return null
|
||||
//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 }
|
||||
}
|
||||
}
|
||||
//Results
|
||||
return followup
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
31
source/plugins/gists/index.mjs
Normal file
31
source/plugins/gists/index.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.gists))
|
||||
return null
|
||||
//Retrieve gists from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > querying api`)
|
||||
const {user:{gists}} = await graphql(queries.gists({login}))
|
||||
//Iterate through gists
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`)
|
||||
let stargazers = 0, forks = 0, comments = 0, files = 0
|
||||
for (const gist of gists.nodes) {
|
||||
//Skip forks
|
||||
if (gist.isFork)
|
||||
continue
|
||||
//Compute stars, forks, comments and files count
|
||||
stargazers += gist.stargazerCount
|
||||
forks += gist.forks.totalCount
|
||||
comments += gist.comments.totalCount
|
||||
files += gist.files.length
|
||||
}
|
||||
//Results
|
||||
return {totalCount:gists.totalCount, stargazers, forks, files, comments}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
112
source/plugins/habits/index.mjs
Normal file
112
source/plugins/habits/index.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
//Setup
|
||||
export default async function ({login, rest, imports, data, q}, {enabled = false, from:defaults = 100} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.habits))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"habits.from":from = defaults.from ?? 500, "habits.days":days = 14, "habits.facts":facts = true, "habits.charts":charts = false} = q
|
||||
//Events
|
||||
from = Math.max(1, Math.min(1000, Number(from)))
|
||||
//Days
|
||||
days = Math.max(1, Math.min(30, Number(from)))
|
||||
//Initialization
|
||||
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
|
||||
const pages = Math.ceil(from/100)
|
||||
const offset = data.config.timezone?.offset ?? 0
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
|
||||
const events = []
|
||||
try {
|
||||
for (let page = 0; page < pages; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`)
|
||||
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
|
||||
}
|
||||
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) }
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
|
||||
//Get user recent commits
|
||||
const commits = events
|
||||
.filter(({type}) => type === "PushEvent")
|
||||
.filter(({actor}) => actor.login === login)
|
||||
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
|
||||
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
|
||||
const patches = [...await Promise.allSettled(commits
|
||||
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
||||
.map(async commit => (await rest.request(commit)).data.files)
|
||||
)]
|
||||
.filter(({status}) => status === "fulfilled")
|
||||
.map(({value}) => value)
|
||||
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), patch:file.patch ?? ""})))
|
||||
.map(({name, patch}) => ({name, patch:patch.split("\n").filter(line => /^[-+]/.test(line)).map(line => line.substring(1)).join("\n")}))
|
||||
//Commit day
|
||||
{
|
||||
//Compute commit days
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active day of week`)
|
||||
const days = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getDay())
|
||||
for (const day of days)
|
||||
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
|
||||
habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
|
||||
//Compute day with most commits
|
||||
habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([an, a], [bn, b]) => b - a).map(([day, occurence]) => day)[0]] ?? NaN : NaN
|
||||
}
|
||||
//Commit hour
|
||||
{
|
||||
//Compute commit hours
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active time of day`)
|
||||
const hours = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getHours())
|
||||
for (const hour of hours)
|
||||
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
||||
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
|
||||
//Compute hour with most commits
|
||||
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
||||
}
|
||||
//Indent style
|
||||
{
|
||||
//Attempt to guess whether tabs or spaces are used in patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`)
|
||||
patches
|
||||
.map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? [])
|
||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
||||
}
|
||||
//Linguist
|
||||
if (charts) {
|
||||
//Check if linguist exists
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
|
||||
const prefix = {win32:"wsl"}[process.platform] ?? ""
|
||||
if ((patches.length)&&(await imports.run(`${prefix} which github-linguist`))) {
|
||||
//Setup for linguist
|
||||
habits.linguist.available = true
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${commits[0]?.actor?.id ?? 0}`)
|
||||
//Create temporary directory and save patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`)
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||
//Create temporary git repository
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
|
||||
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
|
||||
await imports.run(`git status`, {cwd:path})
|
||||
//Spawn linguist process
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||
;(await imports.run(`${prefix} github-linguist --breakdown`, {cwd:path}))
|
||||
//Parse linguist result
|
||||
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>\w+)/)?.groups).filter(line => line)
|
||||
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100)
|
||||
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a)
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
|
||||
}
|
||||
//Results
|
||||
return habits
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
88
source/plugins/isocalendar/index.mjs
Normal file
88
source/plugins/isocalendar/index.mjs
Normal file
@@ -0,0 +1,88 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.isocalendar))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"isocalendar.duration":duration = "half-year"} = q
|
||||
//Duration in days
|
||||
duration = ["full-year", "half-year"].includes(duration) ? duration : "full-year"
|
||||
//Compute start day
|
||||
const now = new Date()
|
||||
const start = new Date(now)
|
||||
if (duration === "full-year")
|
||||
start.setFullYear(now.getFullYear()-1)
|
||||
else
|
||||
start.setHours(-24*180)
|
||||
//Compute padding to ensure last row is complete
|
||||
const padding = new Date(start)
|
||||
padding.setHours(-14*24)
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
|
||||
const calendar = {}
|
||||
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.calendar({login, from:from.toISOString(), to:to.toISOString()}))
|
||||
calendar[name] = weeks
|
||||
}
|
||||
//Apply padding
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
|
||||
const firstweek = calendar.weeks[0].contributionDays
|
||||
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
|
||||
while (firstweek.length < 7)
|
||||
firstweek.unshift(padded.pop())
|
||||
//Compute the highest contributions in a day, streaks and average commits per day
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
|
||||
let max = 0, streak = {max:0, current:0}, values = [], average = 0
|
||||
for (const week of calendar.weeks) {
|
||||
for (const day of week.contributionDays) {
|
||||
values.push(day.contributionCount)
|
||||
max = Math.max(max, day.contributionCount)
|
||||
streak.current = day.contributionCount ? streak.current+1 : 0
|
||||
streak.max = Math.max(streak.max, streak.current)
|
||||
}
|
||||
}
|
||||
average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "")
|
||||
//Compute SVG
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
|
||||
const size = 6
|
||||
let i = 0, j = 0
|
||||
let svg = `
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="margin-top: -52px;" viewBox="0,0 480,${duration === "full-year" ? 270 : 170}">
|
||||
${[1, 2].map(k => `
|
||||
<filter id="brightness${k}">
|
||||
<feComponentTransfer>
|
||||
${[..."RGB"].map(channel => `<feFunc${channel} type="linear" slope="${1-k*0.4}" />`).join("")}
|
||||
</feComponentTransfer>
|
||||
</filter>`
|
||||
).join("")}
|
||||
<g transform="scale(4) translate(12, 0)">`
|
||||
//Iterate through weeks
|
||||
for (const week of calendar.weeks) {
|
||||
svg += `<g transform="translate(${i*1.7}, ${i})">`
|
||||
j = 0
|
||||
//Iterate through days
|
||||
for (const day of week.contributionDays) {
|
||||
const ratio = day.contributionCount/max
|
||||
svg += `
|
||||
<g transform="translate(${j*-1.7}, ${j+(1-ratio)*size})">
|
||||
<path fill="${day.color}" d="M1.7,2 0,1 1.7,0 3.4,1 z" />
|
||||
<path fill="${day.color}" filter="url(#brightness1)" d="M0,1 1.7,2 1.7,${2+ratio*size} 0,${1+ratio*size} z" />
|
||||
<path fill="${day.color}" filter="url(#brightness2)" d="M1.7,2 3.4,1 3.4,${1+ratio*size} 1.7,${2+ratio*size} z" />
|
||||
</g>`
|
||||
j++
|
||||
}
|
||||
svg += `</g>`
|
||||
i++
|
||||
}
|
||||
svg += `</g></svg>`
|
||||
//Results
|
||||
return {streak, max, average, svg, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
49
source/plugins/languages/index.mjs
Normal file
49
source/plugins/languages/index.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
//Setup
|
||||
export default async function ({login, data, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.languages))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"languages.ignored":ignored = "", "languages.skipped":skipped = ""} = q
|
||||
//Ignored languages
|
||||
ignored = decodeURIComponent(ignored).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Skipped repositories
|
||||
skipped = decodeURIComponent(skipped).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Iterate through user's repositories and retrieve languages data
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
|
||||
const languages = {colors:{}, total:0, stats:{}}
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Skip repository if asked
|
||||
if (skipped.includes(repository.name.toLocaleLowerCase())) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > skipped repository ${repository.name}`)
|
||||
continue
|
||||
}
|
||||
//Process repository languages
|
||||
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
|
||||
//Ignore language if asked
|
||||
if (ignored.includes(name.toLocaleLowerCase())) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > ignored language ${name}`)
|
||||
continue
|
||||
}
|
||||
//Update language stats
|
||||
languages.stats[name] = (languages.stats[name] ?? 0) + size
|
||||
languages.colors[name] = color ?? "#ededed"
|
||||
languages.total += size
|
||||
}
|
||||
}
|
||||
//Compute languages stats
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > computing 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
|
||||
//Results
|
||||
return languages
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
37
source/plugins/lines/index.mjs
Normal file
37
source/plugins/lines/index.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
//Setup
|
||||
export default async function ({login, data, imports, rest, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.lines))
|
||||
return null
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
|
||||
//Get contributors stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
|
||||
const lines = {added:0, deleted:0}
|
||||
const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo})))
|
||||
//Compute changed lines
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
|
||||
response.map(({data:repository}) => {
|
||||
//Check if data are available
|
||||
if (!Array.isArray(repository))
|
||||
return
|
||||
//Extract author
|
||||
const [contributor] = repository.filter(({author}) => author.login === login)
|
||||
//Compute editions
|
||||
if (contributor)
|
||||
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
|
||||
})
|
||||
//Format values
|
||||
lines.added = imports.format(lines.added)
|
||||
lines.deleted = imports.format(lines.deleted)
|
||||
//Results
|
||||
return lines
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
194
source/plugins/music/index.mjs
Normal file
194
source/plugins/music/index.mjs
Normal file
@@ -0,0 +1,194 @@
|
||||
//Supported providers
|
||||
const providers = {
|
||||
apple:{
|
||||
name:"Apple Music",
|
||||
embed:/^https:..embed.music.apple.com.\w+.playlist/,
|
||||
},
|
||||
spotify:{
|
||||
name:"Spotify",
|
||||
embed:/^https:..open.spotify.com.embed.playlist/,
|
||||
},
|
||||
}
|
||||
|
||||
//Supported modes
|
||||
const modes = {
|
||||
playlist:"Suggested tracks",
|
||||
recent:"Recently played",
|
||||
}
|
||||
|
||||
//Setup
|
||||
export default async function ({login, imports, q}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.music))
|
||||
return null
|
||||
//Initialization
|
||||
const raw = {
|
||||
get provider() { return providers[provider]?.name ?? "" },
|
||||
get mode() { return modes[mode] ?? "Unconfigured music plugin"},
|
||||
}
|
||||
let tracks = null
|
||||
//Parameters override
|
||||
let {"music.provider":provider = "", "music.mode":mode = "", "music.playlist":playlist = null, "music.limit":limit = 4} = q
|
||||
//Auto-guess parameters
|
||||
if ((playlist)&&(!mode))
|
||||
mode = "playlist"
|
||||
if ((playlist)&&(!provider))
|
||||
for (const [name, {embed}] of Object.entries(providers))
|
||||
if (embed.test(playlist))
|
||||
provider = name
|
||||
if (!mode)
|
||||
mode = "recent"
|
||||
//Provider
|
||||
if (!(provider in providers))
|
||||
throw {error:{message:provider ? `Unsupported provider "${provider}"` : `Missing provider`}, ...raw}
|
||||
//Mode
|
||||
if (!(mode in modes))
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
//Playlist mode
|
||||
if (mode === "playlist") {
|
||||
if (!playlist)
|
||||
throw {error:{message:`Missing playlist url`}, ...raw}
|
||||
if (!providers[provider].embed.test(playlist))
|
||||
throw {error:{message:`Unsupported playlist url format`}, ...raw}
|
||||
}
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
//Handle mode
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
|
||||
switch (mode) {
|
||||
//Playlist mode
|
||||
case "playlist":{
|
||||
//Start puppeteer and navigate to playlist
|
||||
console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
|
||||
const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
|
||||
console.debug(`metrics/compute/${login}/plugins > music > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading page`)
|
||||
await page.goto(playlist)
|
||||
const frame = page.mainFrame()
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Apple music
|
||||
case "apple":{
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector(".tracklist.playlist")
|
||||
tracks = [...await frame.evaluate(() => [...document.querySelectorAll(".tracklist li")].map(li => ({
|
||||
name:li.querySelector(".tracklist__track__name").innerText,
|
||||
artist:li.querySelector(".tracklist__track__sub").innerText,
|
||||
artwork:li.querySelector(".tracklist__track__artwork img").src
|
||||
})))]
|
||||
break
|
||||
}
|
||||
//Spotify
|
||||
case "spotify":{
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector("table")
|
||||
tracks = [...await frame.evaluate(() => [...document.querySelectorAll("table tr")].map(tr => ({
|
||||
name:tr.querySelector("td:nth-child(2) div:nth-child(1)").innerText,
|
||||
artist:tr.querySelector("td:nth-child(2) div:nth-child(2)").innerText,
|
||||
//Spotify doesn't provide artworks so we fallback on playlist artwork instead
|
||||
artwork:window.getComputedStyle(document.querySelector("button[title=Play]").parentNode, null).backgroundImage.match(/^url\("(https:...+)"\)$/)[1]
|
||||
})))]
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
|
||||
console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256}))
|
||||
//Shuffle tracks
|
||||
tracks = imports.shuffle(tracks)
|
||||
}
|
||||
break
|
||||
}
|
||||
//Recently played
|
||||
case "recent":{
|
||||
//Initialisation
|
||||
const timestamp = Date.now()-24*60*60*1000
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Spotify
|
||||
case "spotify":{
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id)||(!client_secret)||(!refresh_token))
|
||||
throw {error:{message:`Spotify token must contain client id/secret and refresh token`}}
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
|
||||
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token",
|
||||
`${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`,
|
||||
{headers:{"Content-Type":"application/x-www-form-urlencoded"}},
|
||||
)
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got access token`)
|
||||
//Retrieve tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
|
||||
tracks = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{
|
||||
"Accept":"application/json",
|
||||
"Content-Type":"application/json",
|
||||
"Authorization":`Bearer ${access}`}
|
||||
})).data.items.map(({track}) => ({
|
||||
name:track.name,
|
||||
artist:track.artists[0].name,
|
||||
artwork:track.album.images[0].url,
|
||||
}))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
}
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Limit tracklist
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`)
|
||||
tracks.splice(limit)
|
||||
}
|
||||
//Convert artworks to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading artworks`)
|
||||
for (const track of tracks) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing ${track.name}`)
|
||||
track.artwork = await imports.imgb64(track.artwork)
|
||||
}
|
||||
//Save results
|
||||
return {...raw, tracks}
|
||||
}
|
||||
//Unhandled error
|
||||
throw {error:{message:`An error occured (could not retrieve tracks)`}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
57
source/plugins/pagespeed/index.mjs
Normal file
57
source/plugins/pagespeed/index.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, q}, {enabled = false, token = null} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.pagespeed)||(!data.user.websiteUrl))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"pagespeed.detailed":detailed = false, "pagespeed.screenshot":screenshot = false} = q
|
||||
//Duration in days
|
||||
detailed = !!detailed
|
||||
//Format url if needed
|
||||
let url = data.user.websiteUrl
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const result = {url, detailed, scores:[], metrics:{}}
|
||||
//Load scores from API
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
|
||||
const scores = new Map()
|
||||
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
|
||||
//Perform audit
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)
|
||||
console.debug(request.data)
|
||||
const {score, title} = request.data.lighthouseResult.categories[category]
|
||||
scores.set(category, {score, title})
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
//Store screenshot
|
||||
if ((screenshot)&&(category === "performance")) {
|
||||
result.screenshot = request.data.lighthouseResult.audits["final-screenshot"].details.data
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
}
|
||||
}))
|
||||
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
|
||||
//Detailed metrics
|
||||
if (detailed) {
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}&key=${token}`)
|
||||
console.debug(request.data)
|
||||
Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items)
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`)
|
||||
}
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
46
source/plugins/posts/index.mjs
Normal file
46
source/plugins/posts/index.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
//Setup
|
||||
export default async function ({imports, data, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.posts))
|
||||
return null
|
||||
//Parameters override
|
||||
const login = data.user.login
|
||||
let {"posts.source":source = "", "posts.limit":limit = 4} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(30, Number(limit)))
|
||||
//Retrieve posts
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
|
||||
let posts = null
|
||||
switch (source) {
|
||||
//Dev.to
|
||||
case "dev.to":{
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${login}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date}))
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported source "${source}"`}}
|
||||
}
|
||||
//Format posts
|
||||
if (Array.isArray(posts)) {
|
||||
//Limit tracklist
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`)
|
||||
posts.splice(limit)
|
||||
}
|
||||
//Results
|
||||
return {source, list:posts}
|
||||
}
|
||||
//Unhandled error
|
||||
throw {error:{message:`An error occured (could not retrieve posts)`}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
59
source/plugins/projects/index.mjs
Normal file
59
source/plugins/projects/index.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
//Setup
|
||||
export default async function ({login, graphql, q, queries}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.projects))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"projects.limit":limit = 4, "projects.repositories":repositories = ""} = q
|
||||
//Repositories projects
|
||||
repositories = repositories?.split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? []
|
||||
//Limit
|
||||
limit = Math.max(repositories.length, Math.min(100, Number(limit)))
|
||||
//Retrieve user owned projects from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
|
||||
const {user:{projects}} = await graphql(queries.projects({login, limit}))
|
||||
//Retrieve repositories projects from graphql api
|
||||
for (const identifier of repositories) {
|
||||
//Querying repository project
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
|
||||
const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups
|
||||
const {user:{repository:{project}}} = await graphql(queries["projects.repository"]({user, repository, id}))
|
||||
//Adding it to projects list
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
|
||||
project.name = `${project.name} (${user}/${repository})`
|
||||
projects.nodes.unshift(project)
|
||||
projects.totalCount++
|
||||
}
|
||||
|
||||
//Iterate through projects and format them
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > processing ${projects.nodes.length} projects`)
|
||||
const list = []
|
||||
for (const project of projects.nodes) {
|
||||
//Format date
|
||||
const time = (Date.now()-new Date(project.updatedAt).getTime())/(24*60*60*1000)
|
||||
let updated = new Date(project.updatedAt).toDateString().substring(4)
|
||||
if (time < 1)
|
||||
updated = "less than 1 day ago"
|
||||
else if (time < 30)
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
//Format progress
|
||||
const {enabled, todoCount:todo, inProgressCount:doing, doneCount:done} = project.progress
|
||||
//Append
|
||||
list.push({name:project.name, updated, progress:{enabled, todo, doing, done, total:todo+doing+done}})
|
||||
}
|
||||
//Limit
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
|
||||
list.splice(limit)
|
||||
//Results
|
||||
return {list, totalCount:projects.totalCount}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
89
source/plugins/topics/index.mjs
Normal file
89
source/plugins/topics/index.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.topics))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"topics.sort":sort = "stars", "topics.mode":mode = "starred", "topics.limit":limit} = q
|
||||
//Shuffle
|
||||
const shuffle = (sort === "random")
|
||||
//Sort method
|
||||
sort = {starred:"created", activity:"updated", stars:"stars", random:"created"}[sort] ?? "starred"
|
||||
//Limit
|
||||
if (!Number.isFinite(limit))
|
||||
limit = (mode === "mastered" ? 0 : 15)
|
||||
limit = Math.max(0, Math.min(20, Number(limit)))
|
||||
//Mode
|
||||
mode = ["starred", "mastered"].includes(mode) ? mode : "starred"
|
||||
//Start puppeteer and navigate to topics
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
|
||||
let topics = []
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > starting browser`)
|
||||
const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
//Iterate through pages
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
//Load page
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading page ${i}`)
|
||||
await page.goto(`https://github.com/stars/${login}/topics?direction=desc&page=${i}&sort=${sort}`)
|
||||
const frame = page.mainFrame()
|
||||
//Extract topics
|
||||
await Promise.race([frame.waitForSelector("ul.repo-list"), frame.waitForSelector(".blankslate")])
|
||||
const starred = await frame.evaluate(() => [...document.querySelectorAll("ul.repo-list li")].map(li => ({
|
||||
name:li.querySelector(".f3").innerText,
|
||||
description:li.querySelector(".f5").innerText,
|
||||
icon:li.querySelector("img")?.src ?? null,
|
||||
})))
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
|
||||
//Check if next page exists
|
||||
if (!starred.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > no more page to load`)
|
||||
break
|
||||
}
|
||||
topics.push(...starred)
|
||||
}
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
//Shuffle topics
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
|
||||
topics = imports.shuffle(topics)
|
||||
}
|
||||
//Limit topics (starred mode)
|
||||
if ((mode === "starred")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
const removed = topics.splice(limit)
|
||||
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
|
||||
}
|
||||
//Convert icons to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
|
||||
for (const topic of topics) {
|
||||
if (topic.icon) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
|
||||
topic.icon = await imports.imgb64(topic.icon)
|
||||
}
|
||||
//Escape HTML description
|
||||
topic.description = imports.htmlescape(topic.description)
|
||||
}
|
||||
//Filter topics with icon (mastered mode)
|
||||
if (mode === "mastered") {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
|
||||
topics = topics.filter(({icon}) => icon)
|
||||
}
|
||||
//Limit topics (mastered mode)
|
||||
if ((mode === "mastered")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
topics.splice(limit)
|
||||
}
|
||||
//Results
|
||||
return {mode, list:topics}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
30
source/plugins/traffic/index.mjs
Normal file
30
source/plugins/traffic/index.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, rest, q}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.traffic))
|
||||
return null
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
|
||||
//Get views stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
|
||||
const views = {count:0, uniques:0}
|
||||
const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo})))
|
||||
//Compute views
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
|
||||
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
|
||||
//Format values
|
||||
views.count = imports.format(views.count)
|
||||
views.uniques = imports.format(views.uniques)
|
||||
//Results
|
||||
return {views}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.status === 403)
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
62
source/plugins/tweets/index.mjs
Normal file
62
source/plugins/tweets/index.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
//Setup
|
||||
export default async function ({login, imports, data, q}, {enabled = false, token = null} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.tweets))
|
||||
return null
|
||||
//Parameters override
|
||||
let {"tweets.limit":limit = 2} = q
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(10, Number(limit)))
|
||||
//Load user profile
|
||||
const username = data.user.twitterUsername
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
|
||||
const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
//Load tweets
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
|
||||
const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
//Load profile image
|
||||
if (profile?.profile_image_url) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading profile image`)
|
||||
profile.profile_image = await imports.imgb64(profile.profile_image_url)
|
||||
}
|
||||
//Limit tweets
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > keeping only ${limit} tweets`)
|
||||
tweets.splice(limit)
|
||||
}
|
||||
//Format tweets
|
||||
await Promise.all(tweets.map(async tweet => {
|
||||
//Mentions
|
||||
tweet.mentions = tweet.entities?.mentions.map(({username}) => username) ?? []
|
||||
//Format text
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`)
|
||||
tweet.text = imports.htmlescape(
|
||||
//Escape tags
|
||||
imports.htmlescape(tweet.text, {"<":true, ">":true})
|
||||
//Mentions
|
||||
.replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), ` <span class="mention">@$1</span> `)
|
||||
//Hashtags (this regex comes from the twitter source code)
|
||||
.replace(/(?<!&)[#|#]([a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*[a-z_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*)/gi, ` <span class="hashtag">#$1</span> `)
|
||||
//Line breaks
|
||||
.replace(/\n/g, "<br/>")
|
||||
//Links
|
||||
.replace(/https?:[/][/](t.co[/]\w+)/g, ` <span class="link">$1</span> `)
|
||||
, {"&":true})
|
||||
}))
|
||||
//Result
|
||||
return {username, profile, list:tweets}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
15
source/queries/calendar.graphql
Normal file
15
source/queries/calendar.graphql
Normal file
@@ -0,0 +1,15 @@
|
||||
query Calendar {
|
||||
user(login: "$login") {
|
||||
calendar:contributionsCollection(from: "$from", to: "$to") {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
contributionCount
|
||||
color
|
||||
date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
source/queries/common.graphql
Normal file
66
source/queries/common.graphql
Normal file
@@ -0,0 +1,66 @@
|
||||
query Metrics {
|
||||
user(login: "$login") {
|
||||
databaseId
|
||||
name
|
||||
login
|
||||
createdAt
|
||||
avatarUrl
|
||||
websiteUrl
|
||||
isHireable
|
||||
twitterUsername
|
||||
repositories(last: 0, isFork: false, ownerAffiliations: OWNER) {
|
||||
totalCount
|
||||
totalDiskUsage
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
packages {
|
||||
totalCount
|
||||
}
|
||||
starredRepositories {
|
||||
totalCount
|
||||
}
|
||||
watching {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsSponsor {
|
||||
totalCount
|
||||
}
|
||||
sponsorshipsAsMaintainer {
|
||||
totalCount
|
||||
}
|
||||
contributionsCollection {
|
||||
totalRepositoriesWithContributedCommits
|
||||
totalCommitContributions
|
||||
restrictedContributionsCount
|
||||
totalIssueContributions
|
||||
totalPullRequestContributions
|
||||
totalPullRequestReviewContributions
|
||||
}
|
||||
calendar:contributionsCollection(from: "$calendar.from", to: "$calendar.to") {
|
||||
contributionCalendar {
|
||||
weeks {
|
||||
contributionDays {
|
||||
color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
repositoriesContributedTo {
|
||||
totalCount
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
following {
|
||||
totalCount
|
||||
}
|
||||
issueComments {
|
||||
totalCount
|
||||
}
|
||||
organizations {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
20
source/queries/gists.graphql
Normal file
20
source/queries/gists.graphql
Normal file
@@ -0,0 +1,20 @@
|
||||
query Gists {
|
||||
user(login: "$login") {
|
||||
gists(last: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
stargazerCount
|
||||
isFork
|
||||
forks {
|
||||
totalCount
|
||||
}
|
||||
files {
|
||||
name
|
||||
}
|
||||
comments {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
source/queries/projects.graphql
Normal file
17
source/queries/projects.graphql
Normal file
@@ -0,0 +1,17 @@
|
||||
query Projects {
|
||||
user(login: "$login") {
|
||||
projects(last: $limit, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
totalCount
|
||||
nodes {
|
||||
name
|
||||
updatedAt
|
||||
progress {
|
||||
doneCount
|
||||
inProgressCount
|
||||
todoCount
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
source/queries/projects.repository.graphql
Normal file
16
source/queries/projects.repository.graphql
Normal file
@@ -0,0 +1,16 @@
|
||||
query RepositoryProject {
|
||||
user(login: "$user") {
|
||||
repository(name: "$repository") {
|
||||
project(number: $id) {
|
||||
name
|
||||
updatedAt
|
||||
progress {
|
||||
doneCount
|
||||
inProgressCount
|
||||
todoCount
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
source/queries/repositories.graphql
Normal file
46
source/queries/repositories.graphql
Normal file
@@ -0,0 +1,46 @@
|
||||
query Repositories {
|
||||
user(login: "$login") {
|
||||
repositories($after first: $repositories, isFork: false, ownerAffiliations: OWNER, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
cursor
|
||||
}
|
||||
nodes {
|
||||
name
|
||||
watchers {
|
||||
totalCount
|
||||
}
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
languages(first: 8) {
|
||||
edges {
|
||||
size
|
||||
node {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
issues_open: issues(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
issues_closed: issues(states: CLOSED) {
|
||||
totalCount
|
||||
}
|
||||
pr_open: pullRequests(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
pr_merged: pullRequests(states: MERGED) {
|
||||
totalCount
|
||||
}
|
||||
releases {
|
||||
totalCount
|
||||
}
|
||||
forkCount
|
||||
licenseInfo {
|
||||
spdxId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
source/queries/repository.graphql
Normal file
43
source/queries/repository.graphql
Normal file
@@ -0,0 +1,43 @@
|
||||
query Repository {
|
||||
user(login: "$login") {
|
||||
repository(name: "$repo") {
|
||||
name
|
||||
createdAt
|
||||
diskUsage
|
||||
watchers {
|
||||
totalCount
|
||||
}
|
||||
stargazers {
|
||||
totalCount
|
||||
}
|
||||
languages(first: 8) {
|
||||
edges {
|
||||
size
|
||||
node {
|
||||
color
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
issues_open: issues(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
issues_closed: issues(states: CLOSED) {
|
||||
totalCount
|
||||
}
|
||||
pr_open: pullRequests(states: OPEN) {
|
||||
totalCount
|
||||
}
|
||||
pr_merged: pullRequests(states: MERGED) {
|
||||
totalCount
|
||||
}
|
||||
releases {
|
||||
totalCount
|
||||
}
|
||||
forkCount
|
||||
licenseInfo {
|
||||
spdxId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0
source/templates/classic/fonts.css
Normal file
0
source/templates/classic/fonts.css
Normal file
807
source/templates/classic/image.svg
Normal file
807
source/templates/classic/image.svg
Normal file
@@ -0,0 +1,807 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 12
|
||||
+ (!!base.header)*80 + (user.isHireable)*16
|
||||
+ (!!base.metadata)*38
|
||||
+ ((!!base.activity)||(!!base.community))*128
|
||||
+ (!!base.repositories)*108
|
||||
+ ((!!base.repositories)*((!!plugins.traffic)||(!!plugins.lines)))*16
|
||||
+ (!!plugins.followup)*68
|
||||
+ (!!plugins.pagespeed)*126 + (plugins.pagespeed?.detailed ?? 0)*6*20 + (!!plugins.pagespeed?.screenshot)*330
|
||||
+ (!!plugins.habits)*28 + (!!plugins.habits?.facts)*58 + (!!plugins.habits?.charts)*226
|
||||
+ (!!plugins.languages)*96
|
||||
+ (!!plugins.music)*64 + (plugins.music?.tracks?.length ? 14+Math.max(0, plugins.music.tracks.length-1)*36 : 0)
|
||||
+ (!!plugins.posts)*64 + (plugins.posts?.list?.length ?? 0)*40
|
||||
+ (!!plugins.isocalendar)*192 + (plugins.isocalendar?.duration === 'full-year')*100
|
||||
+ (!!plugins.gists)*68
|
||||
+ (!!plugins.topics)*160
|
||||
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
|
||||
+ (!!plugins.tweets)*64 + (plugins.tweets?.list?.length ?? 0)*90
|
||||
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!plugins.habits))+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.music)+(!!plugins.posts)+(!!plugins.isocalendar)+(!!plugins.gists)+(!!plugins.topics)+(!!plugins.projects))-1))*4
|
||||
%>">
|
||||
|
||||
<defs><style><%= fonts %></style></defs>
|
||||
|
||||
<style>
|
||||
<%= style %>
|
||||
</style>
|
||||
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<% if (base.header) { %>
|
||||
<section>
|
||||
<h1 class="field">
|
||||
<img class="avatar" src="data:image/png;base64,<%= computed.avatar %>" width="20" height="20" />
|
||||
<span><%= user.name || user.login %></span>
|
||||
</h1>
|
||||
<div class="row">
|
||||
<section>
|
||||
<div class="field <%= computed.cakeday ? 'cakeday' : '' %>">
|
||||
<% if (computed.cakeday) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 1.5a1.25 1.25 0 100 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707zM2 2.75c0 .45.108.875.3 1.25h-.55A1.75 1.75 0 000 5.75v2c0 .698.409 1.3 1 1.582v4.918c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0015 14.25V9.332c.591-.281 1-.884 1-1.582v-2A1.75 1.75 0 0014.25 4h-.55a2.75 2.75 0 00-2.45-4c-.984 0-1.874.42-2.57 1.23A5.086 5.086 0 008 2.274a5.086 5.086 0 00-.68-1.042C6.623.42 5.733 0 4.75 0A2.75 2.75 0 002 2.75zM8.941 4h2.309a1.25 1.25 0 100-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793zm-1.84 1.5H1.75a.25.25 0 00-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-.149zm1.649 0V8h5.5a.25.25 0 00.25-.25v-2a.25.25 0 00-.25-.25h-5.5zm0 4h4.75v4.75a.25.25 0 01-.25.25h-4.5v-5zm-1.5 0v5h-4.5a.25.25 0 01-.25-.25V9.5h4.75z"></path></svg>
|
||||
Joined GitHub <%= computed.registration %>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
|
||||
Joined GitHub <%= computed.registration %>
|
||||
<% } %>
|
||||
</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="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
|
||||
Followed by <%= user.followers.totalCount %> user<%= s(user.followers.totalCount) %>
|
||||
</div>
|
||||
<% if (user.isHireable) { %>
|
||||
<div class="field hire">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.75 0A1.75 1.75 0 005 1.75V3H1.75A1.75 1.75 0 000 4.75v8.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H11V1.75A1.75 1.75 0 009.25 0h-2.5zM9.5 3V1.75a.25.25 0 00-.25-.25h-2.5a.25.25 0 00-.25.25V3h3zM5 4.5H1.75a.25.25 0 00-.25.25V6a2 2 0 002 2h9a2 2 0 002-2V4.75a.25.25 0 00-.25-.25H5zm-1.5 5a3.484 3.484 0 01-2-.627v4.377c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V8.873a3.484 3.484 0 01-2 .627h-9z"></path></svg>
|
||||
Available for hire !
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
<section>
|
||||
<div class="field calendar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <%= computed.calendar.length*15 %> 11" width="<%= computed.calendar.length*15 %>" height="16">
|
||||
<g>
|
||||
<% for (const [x, {color}] of Object.entries(computed.calendar)) { %>
|
||||
<rect class="day" x="<%= x*15 %>" y="0" width="11" height="11" fill="<%= color %>" rx="2" ry="2" />
|
||||
<% } %>
|
||||
</g>
|
||||
</svg>
|
||||
</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="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>
|
||||
Contributed to <%= user.repositoriesContributedTo.totalCount %> repositor<%= s(user.repositoriesContributedTo.totalCount, "y") %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<div class="row">
|
||||
<% if (base.activity) { %>
|
||||
<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="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
Activity
|
||||
</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 %> 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>
|
||||
<%= user.contributionsCollection.totalPullRequestReviewContributions %> Pull request<%= s(user.contributionsCollection.totalPullRequestReviewContributions) %> reviewed
|
||||
</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="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>
|
||||
<%= user.contributionsCollection.totalPullRequestContributions %> Pull request<%= s(user.contributionsCollection.totalPullRequestContributions) %> opened
|
||||
</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="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>
|
||||
<%= user.contributionsCollection.totalIssueContributions %> Issue<%= s(user.contributionsCollection.totalIssueContributions) %> opened
|
||||
</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.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>
|
||||
<%= user.issueComments.totalCount %> issue comment<%= s(user.issueComments.totalCount) %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
<% if (base.community) { %>
|
||||
<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="M1.326 1.973a1.2 1.2 0 011.49-.832c.387.112.977.307 1.575.602.586.291 1.243.71 1.7 1.296.022.027.042.056.061.084A13.22 13.22 0 018 3c.67 0 1.289.037 1.861.108l.051-.07c.457-.586 1.114-1.004 1.7-1.295a9.654 9.654 0 011.576-.602 1.2 1.2 0 011.49.832c.14.493.356 1.347.479 2.29.079.604.123 1.28.07 1.936.541.977.773 2.11.773 3.301C16 13 14.5 15 8 15s-8-2-8-5.5c0-1.034.238-2.128.795-3.117-.08-.712-.034-1.46.052-2.12.122-.943.34-1.797.479-2.29zM8 13.065c6 0 6.5-2 6-4.27C13.363 5.905 11.25 5 8 5s-5.363.904-6 3.796c-.5 2.27 0 4.27 6 4.27z"></path><path d="M4 8a1 1 0 012 0v1a1 1 0 01-2 0V8zm2.078 2.492c-.083-.264.146-.492.422-.492h3c.276 0 .505.228.422.492C9.67 11.304 8.834 12 8 12c-.834 0-1.669-.696-1.922-1.508zM10 8a1 1 0 112 0v1a1 1 0 11-2 0V8z"></path></svg> Community stats
|
||||
</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="M1.5 14.25c0 .138.112.25.25.25H4v-1.25a.75.75 0 01.75-.75h2.5a.75.75 0 01.75.75v1.25h2.25a.25.25 0 00.25-.25V1.75a.25.25 0 00-.25-.25h-8.5a.25.25 0 00-.25.25v12.5zM1.75 16A1.75 1.75 0 010 14.25V1.75C0 .784.784 0 1.75 0h8.5C11.216 0 12 .784 12 1.75v12.5c0 .085-.006.168-.018.25h2.268a.25.25 0 00.25-.25V8.285a.25.25 0 00-.111-.208l-1.055-.703a.75.75 0 11.832-1.248l1.055.703c.487.325.779.871.779 1.456v5.965A1.75 1.75 0 0114.25 16h-3.5a.75.75 0 01-.197-.026c-.099.017-.2.026-.303.026h-3a.75.75 0 01-.75-.75V14h-1v1.25a.75.75 0 01-.75.75h-3zM3 3.75A.75.75 0 013.75 3h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 3.75zM3.75 6a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM3 9.75A.75.75 0 013.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 013 9.75zM7.75 9a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM7 6.75A.75.75 0 017.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 017 6.75zM7.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5z"></path></svg>
|
||||
Member of <%= user.organizations.totalCount %> organization<%= s(user.organizations.totalCount) %>
|
||||
</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="M5.5 3.5a2 2 0 100 4 2 2 0 000-4zM2 5.5a3.5 3.5 0 115.898 2.549 5.507 5.507 0 013.034 4.084.75.75 0 11-1.482.235 4.001 4.001 0 00-7.9 0 .75.75 0 01-1.482-.236A5.507 5.507 0 013.102 8.05 3.49 3.49 0 012 5.5zM11 4a.75.75 0 100 1.5 1.5 1.5 0 01.666 2.844.75.75 0 00-.416.672v.352a.75.75 0 00.574.73c1.2.289 2.162 1.2 2.522 2.372a.75.75 0 101.434-.44 5.01 5.01 0 00-2.56-3.012A3 3 0 0011 4z"></path></svg>
|
||||
Following <%= user.following.totalCount %> user<%= s(user.followers.totalCount) %>
|
||||
</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="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>
|
||||
Sponsoring <%= user.sponsorshipsAsSponsor.totalCount %> repositor<%= s(user.sponsorshipsAsSponsor.totalCount, "y") %>
|
||||
</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="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>
|
||||
Starred <%= user.starredRepositories.totalCount %> repositor<%= s(user.starredRepositories.totalCount, "y") %>
|
||||
</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="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"></path></svg>
|
||||
Watching <%= user.watching.totalCount %> repositor<%= s(user.watching.totalCount, "y") %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (base.repositories) { %>
|
||||
<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="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>
|
||||
<%= user.repositories.totalCount %> Repositor<%= s(user.repositories.totalCount, "y") %>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<section>
|
||||
<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="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>
|
||||
<% if (computed.licenses.favorite.length) { %>
|
||||
Prefer <%= computed.licenses.favorite %> license
|
||||
<% } else { %>
|
||||
No license preference
|
||||
<% } %>
|
||||
</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 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>
|
||||
<%= computed.repositories.releases %> Release<%= s(computed.repositories.releases) %>
|
||||
</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="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>
|
||||
<%= user.packages.totalCount %> Package<%= s(user.packages.totalCount) %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" d="M2.5 3.5c0-.133.058-.318.282-.55.227-.237.592-.484 1.1-.708C4.899 1.795 6.354 1.5 8 1.5c1.647 0 3.102.295 4.117.742.51.224.874.47 1.101.707.224.233.282.418.282.551 0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 5.205 9.646 5.5 8 5.5c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707-.224-.233-.282-.418-.282-.551zM1 3.5c0-.626.292-1.165.7-1.59.406-.422.956-.767 1.579-1.041C4.525.32 6.195 0 8 0c1.805 0 3.475.32 4.722.869.622.274 1.172.62 1.578 1.04.408.426.7.965.7 1.591v9c0 .626-.292 1.165-.7 1.59-.406.422-.956.767-1.579 1.041C11.476 15.68 9.806 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.62-1.579-1.04-.408-.426-.7-.965-.7-1.591v-9zM2.5 8V5.724c.241.15.503.286.779.407C4.525 6.68 6.195 7 8 7c1.805 0 3.475-.32 4.722-.869.275-.121.537-.257.778-.407V8c0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 9.705 9.646 10 8 10c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707C2.558 8.318 2.5 8.133 2.5 8zm0 2.225V12.5c0 .133.058.318.282.55.227.237.592.484 1.1.708 1.016.447 2.471.742 4.118.742 1.647 0 3.102-.295 4.117-.742.51-.224.874-.47 1.101-.707.224-.233.282-.418.282-.551v-2.275c-.241.15-.503.285-.778.406-1.247.549-2.917.869-4.722.869-1.805 0-3.475-.32-4.721-.869a6.236 6.236 0 01-.779-.406z"></path></svg>
|
||||
<%= computed.diskUsage %> used
|
||||
</div>
|
||||
<% if (plugins.lines) { %>
|
||||
<div class="field <%= plugins.lines.error ? 'error' : '' %>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V4.664a.25.25 0 00-.073-.177l-2.914-2.914a.25.25 0 00-.177-.073H2.75zM1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V1.75zm7 1.5a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0V7h-1.5a.75.75 0 010-1.5h1.5V4A.75.75 0 018 3.25zm-3 8a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75z"></path></svg>
|
||||
<% if (plugins.lines.error) { %>
|
||||
<%= plugins.lines.error.message %>
|
||||
<% } else { %>
|
||||
<%= plugins.lines.added %> added, <%= plugins.lines.deleted %> removed
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
<section>
|
||||
<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.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>
|
||||
<%= user.sponsorshipsAsMaintainer.totalCount %> Sponsor<%= s(user.sponsorshipsAsMaintainer.totalCount) %>
|
||||
</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="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>
|
||||
<%= computed.repositories.stargazers %> Stargazer<%= s(computed.repositories.stargazers) %>
|
||||
</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="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>
|
||||
<%= computed.repositories.forks %> Fork<%= s(computed.repositories.forks) %>
|
||||
</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="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"></path></svg>
|
||||
<%= computed.repositories.watchers %> Watcher<%= s(computed.repositories.watchers) %>
|
||||
</div>
|
||||
<% if (plugins.traffic) { %>
|
||||
<div class="field <%= plugins.traffic.error ? 'error' : '' %>">
|
||||
<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>
|
||||
<% if (plugins.traffic.error) { %>
|
||||
<%= plugins.traffic.error.message %>
|
||||
<% } else { %>
|
||||
<%= plugins.traffic.views.count %> view<%= s(plugins.traffic.views.count) %> in last two weeks
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.followup) { %>
|
||||
<div class="row">
|
||||
|
||||
<section class="column">
|
||||
<h3>Issues</h3>
|
||||
<% if (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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.followup.error.message %>
|
||||
</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="<%= plugins.followup.issues.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#d73a49"/>
|
||||
<rect mask="url(#issues-bar)" x="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.issues.closed/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"><%= 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"><%= plugins.followup.issues.open %> Open</span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
|
||||
<section class="column">
|
||||
<h3>Pull requests</h3>
|
||||
<% if (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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.followup.error.message %>
|
||||
</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="<%= plugins.followup.pr.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#6f42c1"/>
|
||||
<rect mask="url(#pr-bar)" x="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.pr.merged/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"><%= 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"><%= plugins.followup.pr.open %> Open</span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.languages) { %>
|
||||
<section class="column">
|
||||
<h3>Most used languages</h3>
|
||||
<% if (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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.languages.error.message %>
|
||||
</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="<%= plugins.languages.favorites.length ? 0 : 460 %>" height="8" fill="#d1d5da"/>
|
||||
<% for (const {name, value, color, x} of 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 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 (plugins.projects) { %>
|
||||
<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="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
|
||||
<%= plugins.projects.totalCount %> Project<%= s(plugins.projects.totalCount) %>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<% if (plugins.projects.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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.projects.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
<section>
|
||||
<% for (const {name, updated, progress} of plugins.projects.list) { %>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<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 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
|
||||
<%= name %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="row">
|
||||
<section>
|
||||
<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="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
|
||||
Updated <%= updated %>
|
||||
</div>
|
||||
</section>
|
||||
<% if (progress.enabled) { %>
|
||||
<section>
|
||||
<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>
|
||||
<%= [progress.done ? `${progress.done} done` : "", progress.doing ? `${progress.doing} doing` : "", progress.todo ? `${progress.todo} todo` : ""].filter(str => str).join(" · ") %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (progress.enabled) { %>
|
||||
<div class="field center horizontal-wrap ">
|
||||
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
|
||||
<mask id="project-bar">
|
||||
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
<rect mask="url(#project-bar)" x="0" y="0" width="<%= (progress.done/progress.total)*460 %>" height="8" fill="#28A745"/>
|
||||
<rect mask="url(#project-bar)" x="<%= (progress.done/progress.total)*460 %>" y="0" width="<%= (progress.doing/progress.total)*460 %>" height="8" fill="#6F42C1"/>
|
||||
<rect mask="url(#project-bar)" x="<%= ((progress.done+progress.doing)/progress.total)*460 %>" y="0" width="<%= (progress.todo/progress.total)*460 %>" height="8" fill="#d1d5da"/>
|
||||
</svg>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.gists) { %>
|
||||
<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="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"></path></svg>
|
||||
<%= plugins.gists.totalCount %> Gist<%= s(plugins.gists.totalCount) %>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<% if (plugins.gists.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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.gists.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
<section>
|
||||
<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 1.75C4 .784 4.784 0 5.75 0h5.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v8.586A1.75 1.75 0 0114.25 15h-9a.75.75 0 010-1.5h9a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 0110 4.25V1.5H5.75a.25.25 0 00-.25.25v2.5a.75.75 0 01-1.5 0v-2.5zm7.5-.188V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM5.72 6.72a.75.75 0 000 1.06l1.47 1.47-1.47 1.47a.75.75 0 101.06 1.06l2-2a.75.75 0 000-1.06l-2-2a.75.75 0 00-1.06 0zM3.28 7.78a.75.75 0 00-1.06-1.06l-2 2a.75.75 0 000 1.06l2 2a.75.75 0 001.06-1.06L1.81 9.25l1.47-1.47z"></path></svg>
|
||||
<%= plugins.gists.files %> File<%= s(plugins.gists.files) %>
|
||||
</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.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>
|
||||
<%= plugins.gists.comments %> Comment<%= s(plugins.gists.comments) %>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<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="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>
|
||||
<%= plugins.gists.stargazers %> Stargazer<%= s(plugins.gists.stargazers) %>
|
||||
</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="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>
|
||||
<%= plugins.gists.forks %> Fork<%= s(plugins.gists.forks) %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</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>
|
||||
<%= user.websiteUrl %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<% if (plugins.pagespeed.error) { %>
|
||||
<div class="row">
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.pagespeed.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<section>
|
||||
<div class="row fill-width">
|
||||
<section class="categories">
|
||||
<% for (const {score, title} of plugins.pagespeed.scores) { %>
|
||||
<div class="categorie column">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="50" height="50" class="gauge <%= !Number.isNaN(score) ? (score >= 0.9 ? 'high' : score >= 0.5 ? 'average' : 'low') : '' %>">
|
||||
<circle class="gauge-base" r="53" cx="60" cy="60"></circle>
|
||||
<% if (!Number.isNaN(score)) { %>
|
||||
<circle class="gauge-arc" transform="rotate(-90 60 60)" r="53" cx="60" cy="60" stroke-dasharray="<%= score * 329 %> 329"></circle>
|
||||
<text x="60" y="60" dominant-baseline="central" ><%= Math.round(score*100) %></text>
|
||||
<% } else { %>
|
||||
<text x="60" y="60" dominant-baseline="central" >-</text>
|
||||
<% } %>
|
||||
</svg>
|
||||
<span class="title"><%= title %></span>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% if (plugins.pagespeed.detailed) { %>
|
||||
<div class="audits row">
|
||||
<section>
|
||||
<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="M5.75.75A.75.75 0 016.5 0h3a.75.75 0 010 1.5h-.75v1l-.001.041a6.718 6.718 0 013.464 1.435l.007-.006.75-.75a.75.75 0 111.06 1.06l-.75.75-.006.007a6.75 6.75 0 11-10.548 0L2.72 5.03l-.75-.75a.75.75 0 011.06-1.06l.75.75.007.006A6.718 6.718 0 017.25 2.541a.756.756 0 010-.041v-1H6.5a.75.75 0 01-.75-.75zM8 14.5A5.25 5.25 0 108 4a5.25 5.25 0 000 10.5zm.389-6.7l1.33-1.33a.75.75 0 111.061 1.06L9.45 8.861A1.502 1.502 0 018 10.75a1.5 1.5 0 11.389-2.95z"></path></svg>
|
||||
Time to interactive
|
||||
</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="M8 1.5a6.5 6.5 0 106.016 4.035.75.75 0 011.388-.57 8 8 0 11-4.37-4.37.75.75 0 01-.569 1.389A6.479 6.479 0 008 1.5zm6.28.22a.75.75 0 010 1.06l-4.063 4.064a2.5 2.5 0 11-1.06-1.06L13.22 1.72a.75.75 0 011.06 0zM7 8a1 1 0 112 0 1 1 0 01-2 0z"></path></svg>
|
||||
Speed Index
|
||||
</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="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>
|
||||
Total Blocking Time
|
||||
</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="M11.134 1.535C9.722 2.562 8.16 4.057 6.889 5.312 5.8 6.387 5.041 7.401 4.575 8.294a3.745 3.745 0 00-3.227 1.054c-.43.431-.69 1.066-.86 1.657a11.982 11.982 0 00-.358 1.914A21.263 21.263 0 000 15.203v.054l.75-.007-.007.75h.054a14.404 14.404 0 00.654-.012 21.243 21.243 0 001.63-.118c.62-.07 1.3-.18 1.914-.357.592-.17 1.226-.43 1.657-.861a3.745 3.745 0 001.055-3.217c.908-.461 1.942-1.216 3.04-2.3 1.279-1.262 2.764-2.825 3.775-4.249.501-.706.923-1.428 1.125-2.096.2-.659.235-1.469-.368-2.07-.606-.607-1.42-.55-2.069-.34-.66.213-1.376.646-2.076 1.155zm-3.95 8.48a3.76 3.76 0 00-1.19-1.192 9.758 9.758 0 011.161-1.607l1.658 1.658a9.853 9.853 0 01-1.63 1.142zM.742 16l.007-.75-.75.008A.75.75 0 00.743 16zM12.016 2.749c-1.224.89-2.605 2.189-3.822 3.384l1.718 1.718c1.21-1.205 2.51-2.597 3.387-3.833.47-.662.78-1.227.912-1.662.134-.444.032-.551.009-.575h-.001V1.78c-.014-.014-.112-.113-.548.027-.432.14-.995.462-1.655.942zM1.62 13.089a19.56 19.56 0 00-.104 1.395 19.55 19.55 0 001.396-.104 10.528 10.528 0 001.668-.309c.526-.151.856-.325 1.011-.48a2.25 2.25 0 00-3.182-3.182c-.155.155-.329.485-.48 1.01a10.515 10.515 0 00-.309 1.67z"></path></svg>
|
||||
First Contentful Paint
|
||||
</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="M1.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h.94a.76.76 0 01.03-.03l6.077-6.078a1.75 1.75 0 012.412-.06L14.5 10.31V2.75a.25.25 0 00-.25-.25H1.75zm12.5 11H4.81l5.048-5.047a.25.25 0 01.344-.009l4.298 3.889v.917a.25.25 0 01-.25.25zm1.75-.25V2.75A1.75 1.75 0 0014.25 1H1.75A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25zM5.5 6a.5.5 0 11-1 0 .5.5 0 011 0zM7 6a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
Largest Contentful Paint
|
||||
</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="M7.75 14A1.75 1.75 0 016 12.25v-8.5C6 2.784 6.784 2 7.75 2h6.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14h-6.5zm-.25-1.75c0 .138.112.25.25.25h6.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25h-6.5a.25.25 0 00-.25.25v8.5zM4.9 3.508a.75.75 0 01-.274 1.025.25.25 0 00-.126.217v6.5a.25.25 0 00.126.217.75.75 0 01-.752 1.298A1.75 1.75 0 013 11.25v-6.5c0-.649.353-1.214.874-1.516a.75.75 0 011.025.274zM1.625 5.533a.75.75 0 10-.752-1.299A1.75 1.75 0 000 5.75v4.5c0 .649.353 1.214.874 1.515a.75.75 0 10.752-1.298.25.25 0 01-.126-.217v-4.5a.25.25 0 01.126-.217z"></path></svg>
|
||||
Cumulative Layout Shift
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<% for (const {score, suffix = "", threshold} of [
|
||||
{score:plugins.pagespeed.metrics.interactive/1000, suffix:"s", threshold:[3.785, 7.3]},
|
||||
{score:plugins.pagespeed.metrics.speedIndex/1000, suffix:"s", threshold:[3.387, 5.8]},
|
||||
{score:plugins.pagespeed.metrics.totalBlockingTime/1000, suffix:"s", threshold:[.287, .6]},
|
||||
{score:plugins.pagespeed.metrics.firstContentfulPaint/1000, suffix:"s", threshold:[2.336, 4]},
|
||||
{score:plugins.pagespeed.metrics.largestContentfulPaint/1000, suffix:"s", threshold:[2.5, 4]},
|
||||
{score:+plugins.pagespeed.metrics.cumulativeLayoutShift, threshold:[.1, .25]}
|
||||
]) { %>
|
||||
<div class="field">
|
||||
<div class="audit text">
|
||||
<% if (!Number.isNaN(score)) { %>
|
||||
<%= score.toFixed(2).replace(/[.]0+$/, "") %> <%= suffix %>
|
||||
<% } else { %>
|
||||
N/A
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (Number.isNaN(score)) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="audit" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm3.28 5.78a.75.75 0 00-1.06-1.06l-5.5 5.5a.75.75 0 101.06 1.06l5.5-5.5z"></path></svg>
|
||||
<% } else if (score <= threshold[0]) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="audit high" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM0 8a8 8 0 1116 0A8 8 0 010 8zm11.78-1.72a.75.75 0 00-1.06-1.06L6.75 9.19 5.28 7.72a.75.75 0 00-1.06 1.06l2 2a.75.75 0 001.06 0l4.5-4.5z"></path></svg>
|
||||
<% } else if (score <= threshold[1]) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="audit average" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 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>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="audit low" 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>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (plugins.pagespeed.screenshot) { %>
|
||||
<div class="row">
|
||||
<section>
|
||||
<img class="screenshot" src="<%= plugins.pagespeed.screenshot %>" width="452" height="315"/>
|
||||
</section>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% 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 and recent activity
|
||||
</h2>
|
||||
<% if (plugins.habits.facts) { %>
|
||||
<div class="row">
|
||||
<% if (plugins.habits.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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.habits.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
<ul class="habits">
|
||||
<% if (plugins.habits.indents.style) { %>
|
||||
<li>Use <%= plugins.habits.indents.style %> for indents</li>
|
||||
<% } %>
|
||||
<% if (!Number.isNaN(plugins.habits.commits.hour)) { %>
|
||||
<li>Mostly push code around <%= plugins.habits.commits.hour %>:00</li>
|
||||
<% } %>
|
||||
<% if (plugins.habits.commits.day) { %>
|
||||
<li>Mostly active on <%= plugins.habits.commits.day.toLocaleLowerCase() %></li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
|
||||
<% if (plugins.habits.charts) { %>
|
||||
<% if (!Number.isNaN(plugins.habits.commits.hour)) { %>
|
||||
<section class="column chart">
|
||||
<h3>Commit activity per time of the day</h3>
|
||||
<div class="chart-bars">
|
||||
<% for (let h = 0; h < 24; h++) { const p = (plugins.habits.commits.hours[h]??0)/(plugins.habits.commits.hours.max??1); %>
|
||||
<div class="entry">
|
||||
<span class="value"><%= plugins.habits.commits.hours[h] %></span>
|
||||
<div class="bar" style="height: <%= p*50 %>px; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||
<%= `${h}`.padStart(2, 0) %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<div class="row">
|
||||
<% if (!Number.isNaN(plugins.habits.commits.day)) { %>
|
||||
<section class="column chart">
|
||||
<h3>Commit activity per day</h3>
|
||||
<div class="chart-bars">
|
||||
<% for (let d = 0; d < 7; d++) { const p = (plugins.habits.commits.days[d]??0)/(plugins.habits.commits.days.max??1); %>
|
||||
<div class="entry">
|
||||
<span class="value"><%= plugins.habits.commits.days[d] %></span>
|
||||
<div class="bar" style="height: <%= p*50 %>px; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||
<%= ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d] %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
<% if (plugins.habits.linguist.available) { %>
|
||||
<section class="column chart">
|
||||
<h3>Language activity</h3>
|
||||
<div class="chart-bars horizontal">
|
||||
<% for (const [language, p] of plugins.habits.linguist.ordered) { %>
|
||||
<div class="entry">
|
||||
<span class="name"><%= language %></span>
|
||||
<div class="bar" style="width: <%= p*80 %>%; background-color: var(--color-calendar-graph-day-L<%= Math.ceil(p/0.25) %>-bg)"></div>
|
||||
<span class="value"><%= Math.round(100*p) %>%</span>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.topics) { %>
|
||||
<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.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:"Starred topics", mastered:"Mastered technologies and topics"}[plugins.topics.mode] %>
|
||||
</h2>
|
||||
<div class="row">
|
||||
<% if (plugins.topics.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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.topics.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
<section>
|
||||
<div class="topics fill-width">
|
||||
<% if (plugins.topics.mode === "starred") { %>
|
||||
<% for (const {name, description} of plugins.topics.list) { %>
|
||||
<div class="label" title="<%= description %>"><%= name.toLocaleLowerCase() %></div>
|
||||
<% } %>
|
||||
<% } else if (plugins.topics.mode === "mastered") { %>
|
||||
<% for (const {name, icon} of plugins.topics.list) { %>
|
||||
<% if (icon) { %>
|
||||
<img src="data:image/png;base64,<%= icon %>" width="24" height="24" alt="<%= name %>" title="<%= name %>"/>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.music) { %>
|
||||
<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="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>
|
||||
<%= plugins.music.mode %>
|
||||
</h2>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<% if (plugins.music.provider) { %>
|
||||
<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.002 2.725a.75.75 0 01.797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 11-1.497.098 10.502 10.502 0 00-9.776-9.776.75.75 0 01-.7-.798zM2 13a1 1 0 112 0 1 1 0 01-2 0zm.84-5.95a.75.75 0 00-.179 1.489c2.509.3 4.5 2.291 4.8 4.8a.75.75 0 101.49-.178A7.003 7.003 0 002.838 7.05z"></path></svg>
|
||||
From <%= plugins.music.provider %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (plugins.music.error) { %>
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.music.error.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<% if (plugins.music.tracks.length) { %>
|
||||
<div class="tracklist">
|
||||
<% for (const {name = "", artist = "", artwork = ""} of plugins.music.tracks) { %>
|
||||
<div class="track">
|
||||
<img src="data:image/png;base64,<%= artwork %>" width="32" height="32" alt=""/>
|
||||
<div class="infos">
|
||||
<div class="name"><%= name %></div>
|
||||
<div class="artist"><%= artist %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<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="M1.75 1.5a.25.25 0 00-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0114.25 13H8.06l-2.573 2.573A1.457 1.457 0 013 14.543V13H1.75A1.75 1.75 0 010 11.25v-9.5zM9 9a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
|
||||
No music recently listened
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.posts) { %>
|
||||
<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="M4.456.734a1.75 1.75 0 012.826.504l.613 1.327a3.081 3.081 0 002.084 1.707l2.454.584c1.332.317 1.8 1.972.832 2.94L11.06 10l3.72 3.72a.75.75 0 11-1.061 1.06L10 11.06l-2.204 2.205c-.968.968-2.623.5-2.94-.832l-.584-2.454a3.081 3.081 0 00-1.707-2.084l-1.327-.613a1.75 1.75 0 01-.504-2.826L4.456.734zM5.92 1.866a.25.25 0 00-.404-.072L1.794 5.516a.25.25 0 00.072.404l1.328.613A4.582 4.582 0 015.73 9.63l.584 2.454a.25.25 0 00.42.12l5.47-5.47a.25.25 0 00-.12-.42L9.63 5.73a4.581 4.581 0 01-3.098-2.537L5.92 1.866z"></path></svg>
|
||||
Recent articles
|
||||
</h2>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<% if (plugins.posts.error) { %>
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.posts.error.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<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.002 2.725a.75.75 0 01.797-.699C8.79 2.42 13.58 7.21 13.974 13.201a.75.75 0 11-1.497.098 10.502 10.502 0 00-9.776-9.776.75.75 0 01-.7-.798zM2 13a1 1 0 112 0 1 1 0 01-2 0zm.84-5.95a.75.75 0 00-.179 1.489c2.509.3 4.5 2.291 4.8 4.8a.75.75 0 101.49-.178A7.003 7.003 0 002.838 7.05z"></path></svg>
|
||||
From <%= plugins.posts.source %>
|
||||
</div>
|
||||
<% if (plugins.posts.list.length) { %>
|
||||
<% for (const {title, date} of plugins.posts.list) { %>
|
||||
<div class="field post">
|
||||
<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>
|
||||
<div class="infos">
|
||||
<div class="date"><%= date %></div>
|
||||
<div class="title"><%= title %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<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="M1.75 1.5a.25.25 0 00-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0114.25 13H8.06l-2.573 2.573A1.457 1.457 0 013 14.543V13H1.75A1.75 1.75 0 010 11.25v-9.5zM9 9a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
|
||||
No recent posts
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.tweets) { %>
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 273.5 222.3" width="16" height="16"><path d="M273.5 26.3a109.77 109.77 0 0 1-32.2 8.8 56.07 56.07 0 0 0 24.7-31 113.39 113.39 0 0 1-35.7 13.6 56.1 56.1 0 0 0-97 38.4 54 54 0 0 0 1.5 12.8A159.68 159.68 0 0 1 19.1 10.3a56.12 56.12 0 0 0 17.4 74.9 56.06 56.06 0 0 1-25.4-7v.7a56.11 56.11 0 0 0 45 55 55.65 55.65 0 0 1-14.8 2 62.39 62.39 0 0 1-10.6-1 56.24 56.24 0 0 0 52.4 39 112.87 112.87 0 0 1-69.7 24 119 119 0 0 1-13.4-.8 158.83 158.83 0 0 0 86 25.2c103.2 0 159.6-85.5 159.6-159.6 0-2.4-.1-4.9-.2-7.3a114.25 114.25 0 0 0 28.1-29.1"></path></svg>
|
||||
Latest tweets
|
||||
</h2>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<% if (plugins.tweets.error) { %>
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.tweets.error.message %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="field <%= !plugins.tweets.profile ? 'error' : '' %>">
|
||||
<% if (plugins.tweets.profile?.verified) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.585.52a2.678 2.678 0 00-3.17 0l-.928.68a1.178 1.178 0 01-.518.215L3.83 1.59a2.678 2.678 0 00-2.24 2.24l-.175 1.14a1.178 1.178 0 01-.215.518l-.68.928a2.678 2.678 0 000 3.17l.68.928c.113.153.186.33.215.518l.175 1.138a2.678 2.678 0 002.24 2.24l1.138.175c.187.029.365.102.518.215l.928.68a2.678 2.678 0 003.17 0l.928-.68a1.17 1.17 0 01.518-.215l1.138-.175a2.678 2.678 0 002.241-2.241l.175-1.138c.029-.187.102-.365.215-.518l.68-.928a2.678 2.678 0 000-3.17l-.68-.928a1.179 1.179 0 01-.215-.518L14.41 3.83a2.678 2.678 0 00-2.24-2.24l-1.138-.175a1.179 1.179 0 01-.518-.215L9.585.52zM7.303 1.728c.415-.305.98-.305 1.394 0l.928.68c.348.256.752.423 1.18.489l1.136.174c.51.078.909.478.987.987l.174 1.137c.066.427.233.831.489 1.18l.68.927c.305.415.305.98 0 1.394l-.68.928a2.678 2.678 0 00-.489 1.18l-.174 1.136a1.178 1.178 0 01-.987.987l-1.137.174a2.678 2.678 0 00-1.18.489l-.927.68c-.415.305-.98.305-1.394 0l-.928-.68a2.678 2.678 0 00-1.18-.489l-1.136-.174a1.178 1.178 0 01-.987-.987l-.174-1.137a2.678 2.678 0 00-.489-1.18l-.68-.927a1.178 1.178 0 010-1.394l.68-.928c.256-.348.423-.752.489-1.18l.174-1.136c.078-.51.478-.909.987-.987l1.137-.174a2.678 2.678 0 001.18-.489l.927-.68zM11.28 6.78a.75.75 0 00-1.06-1.06L7 8.94 5.78 7.72a.75.75 0 00-1.06 1.06l1.75 1.75a.75.75 0 001.06 0l3.75-3.75z"></path></svg>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 2.37a6.5 6.5 0 006.5 11.26.75.75 0 01.75 1.298 8 8 0 113.994-7.273.754.754 0 01.006.095v1.5a2.75 2.75 0 01-5.072 1.475A4 4 0 1112 8v1.25a1.25 1.25 0 002.5 0V7.867a6.5 6.5 0 00-9.75-5.496V2.37zM10.5 8a2.5 2.5 0 10-5 0 2.5 2.5 0 005 0z"></path></svg>
|
||||
<% } %>
|
||||
<%= plugins.tweets.username %>
|
||||
<% if (!plugins.tweets.profile) { %>
|
||||
: twitter username not found
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (plugins.tweets.profile) { %>
|
||||
<% if (plugins.tweets.list.length) { %>
|
||||
<% for (const {text, created_at} of plugins.tweets.list) { %>
|
||||
<div class="tweet">
|
||||
<%- text %>
|
||||
<div class="date"><%= new Date(created_at).toGMTString() %></div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<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="M1.75 1.5a.25.25 0 00-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0114.25 13H8.06l-2.573 2.573A1.457 1.457 0 013 14.543V13H1.75A1.75 1.75 0 010 11.25v-9.5zM9 9a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
|
||||
No recent tweets
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.isocalendar) { %>
|
||||
<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="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>
|
||||
Contributions calendar
|
||||
</h2>
|
||||
<div class="row">
|
||||
<section>
|
||||
<% if (plugins.isocalendar.error) { %>
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.isocalendar.error.message %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
<% if (!plugins.isocalendar.error) { %>
|
||||
<section>
|
||||
<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.998 14.5c2.832 0 5-1.98 5-4.5 0-1.463-.68-2.19-1.879-3.383l-.036-.037c-1.013-1.008-2.3-2.29-2.834-4.434-.322.256-.63.579-.864.953-.432.696-.621 1.58-.046 2.73.473.947.67 2.284-.278 3.232-.61.61-1.545.84-2.403.633a2.788 2.788 0 01-1.436-.874A3.21 3.21 0 003 10c0 2.53 2.164 4.5 4.998 4.5zM9.533.753C9.496.34 9.16.009 8.77.146 7.035.75 4.34 3.187 5.997 6.5c.344.689.285 1.218.003 1.5-.419.419-1.54.487-2.04-.832-.173-.454-.659-.762-1.035-.454C2.036 7.44 1.5 8.702 1.5 10c0 3.512 2.998 6 6.498 6s6.5-2.5 6.5-6c0-2.137-1.128-3.26-2.312-4.438-1.19-1.184-2.436-2.425-2.653-4.81z"></path></svg>
|
||||
Current streak <%= plugins.isocalendar.streak.current %> day<%= s(plugins.isocalendar.streak.current) %>
|
||||
</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="M1.5 1.75a.75.75 0 00-1.5 0v12.5c0 .414.336.75.75.75h14.5a.75.75 0 000-1.5H1.5V1.75zm14.28 2.53a.75.75 0 00-1.06-1.06L10 7.94 7.53 5.47a.75.75 0 00-1.06 0L3.22 8.72a.75.75 0 001.06 1.06L7 7.06l2.47 2.47a.75.75 0 001.06 0l5.25-5.25z"></path></svg>
|
||||
~<%= plugins.isocalendar.average %> commits per day
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (plugins.isocalendar.svg) { %>
|
||||
<%- plugins.isocalendar.svg %>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (base.metadata) { %>
|
||||
<footer>
|
||||
<span>These metrics <%= !computed.token.scopes.includes("repo") ? "does not include all" : "includes" %> private contributions<% if ((config.timezone?.name)&&(!config.timezone?.error)) { %>, timezone <%= config.timezone.name %><% } %></span>
|
||||
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
|
||||
</footer>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 75 KiB |
424
source/templates/classic/style.css
Normal file
424
source/templates/classic/style.css
Normal file
@@ -0,0 +1,424 @@
|
||||
/* 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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.field svg {
|
||||
margin: 0 8px;
|
||||
fill: #959da5;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.field.error {
|
||||
color: #cb2431;
|
||||
}
|
||||
.field.error svg {
|
||||
fill: #cb2431;
|
||||
}
|
||||
|
||||
/* 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: #000000;
|
||||
border-radius: 50%;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.field.language small {
|
||||
margin-left: 4px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.label {
|
||||
background-color: #F1F8FF;
|
||||
color: #0366D6;
|
||||
padding: 0 10px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
margin: 2px 5px;
|
||||
white-space: nowrap;
|
||||
border-radius: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.label:hover {
|
||||
background-color: #DDEEFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Habits */
|
||||
.habits {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
padding-left: 37px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 8px;
|
||||
font-size: 10px;
|
||||
font-style: italic;
|
||||
color: #666666;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Speed test categories */
|
||||
.categories {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.categorie {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
/* Gauges */
|
||||
.gauge {
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
}
|
||||
.gauge.high {
|
||||
color: #18b663;
|
||||
}
|
||||
.gauge.average {
|
||||
color: #fb8c00;
|
||||
}
|
||||
.gauge.low {
|
||||
color: #e53935;
|
||||
}
|
||||
.gauge-base, .gauge-arc {
|
||||
stroke: currentColor;
|
||||
stroke-width: 10;
|
||||
}
|
||||
.gauge-base {
|
||||
stroke-opacity: .2;
|
||||
}
|
||||
.gauge-arc {
|
||||
fill: none;
|
||||
stroke-dashoffset: 0;
|
||||
animation-delay: 250ms;
|
||||
animation: animation-gauge 1s ease forwards
|
||||
}
|
||||
.gauge text {
|
||||
fill: currentColor;
|
||||
font-size: 40px;
|
||||
font-family: monospace;
|
||||
text-anchor: middle;
|
||||
font-weight: 600;
|
||||
}
|
||||
.gauge .title {
|
||||
font-size: 18px;
|
||||
color: #777777;
|
||||
}
|
||||
@keyframes animation-gauge {
|
||||
from {
|
||||
stroke-dasharray: 0 329;
|
||||
}
|
||||
}
|
||||
.audits {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.audit.text {
|
||||
min-width: 42px;
|
||||
}
|
||||
.audit svg {
|
||||
margin: 0;
|
||||
}
|
||||
.audit.high {
|
||||
fill: #18b663;
|
||||
}
|
||||
.audit.average {
|
||||
fill: #fb8c00;
|
||||
}
|
||||
.audit.low {
|
||||
fill: #e53935;
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
width: 452px;
|
||||
height: 315px;
|
||||
margin: 8px 14px 4px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Music plugin */
|
||||
.tracklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-left: 28px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
.track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.track img {
|
||||
margin: 0 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
.track .name {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.track .artist {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Posts plugin */
|
||||
.post {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.post .infos {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.post .infos .title {
|
||||
font-size: 14px;
|
||||
width: 400px;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 40px;;
|
||||
}
|
||||
.post .infos .date {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
width: 40px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
/* Topics */
|
||||
.topics {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topics img {
|
||||
border-radius: 5px;
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
/* Tweets */
|
||||
.tweet {
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 18px;
|
||||
border-left: 3px solid #777777B2;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.tweet .mention, .tweet .link, .tweet .hashtag {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
.tweet .date {
|
||||
margin: 6px 0;
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* Charts and graphs */
|
||||
.chart {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.chart-bars .entry {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.chart-bars .entry .value {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.chart-bars .bar {
|
||||
width: 7px;
|
||||
background-color: var(--color-calendar-graph-day-bg);
|
||||
border: 1px solid var(--color-calendar-graph-day-border);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.chart-bars.horizontal {
|
||||
flex-direction: column;
|
||||
align-items: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-bars.horizontal .entry {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chart-bars.horizontal .entry .name {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
min-width: 30%;
|
||||
}
|
||||
|
||||
.chart-bars.horizontal .bar {
|
||||
height: 7px;
|
||||
width: auto;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
/* Fade animation */
|
||||
.af {
|
||||
opacity: 0;
|
||||
animation: animation-fade 1s ease forwards;
|
||||
}
|
||||
@keyframes animation-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cake day */
|
||||
.cakeday, .cakeday svg {
|
||||
animation: animation-rainbow 1.2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: steps(1);
|
||||
}
|
||||
|
||||
/* Rainbow animation */
|
||||
@keyframes animation-rainbow {
|
||||
0%, 100%{ color: #7F00FF; fill: #7F00FF; }
|
||||
14% { color: #A933FF; fill: #A933FF; }
|
||||
29%{ color: #007FFF; fill: #007FFF; }
|
||||
43%{ color: #00FF7F; fill: #00FF7F; }
|
||||
57%{ color: #FFFF00; fill: #FFFF00; }
|
||||
71%{ color: #FF7F00; fill: #FF7F00; }
|
||||
86%{ color: #FF0000; fill: #FF0000; }
|
||||
}
|
||||
|
||||
/* Calendar */
|
||||
:root {
|
||||
--color-calendar-graph-day-bg: #ebedf0;
|
||||
--color-calendar-graph-day-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-bg: #9be9a8;
|
||||
--color-calendar-graph-day-L2-bg: #40c463;
|
||||
--color-calendar-graph-day-L3-bg: #30a14e;
|
||||
--color-calendar-graph-day-L4-bg: #216e39;
|
||||
--color-calendar-halloween-graph-day-L1-bg: #ffee4a;
|
||||
--color-calendar-halloween-graph-day-L2-bg: #ffc501;
|
||||
--color-calendar-halloween-graph-day-L3-bg: #fe9600;
|
||||
--color-calendar-halloween-graph-day-L4-bg: #03001c;
|
||||
--color-calendar-graph-day-L4-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L3-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L2-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-border: rgba(27,31,35,0.06);
|
||||
}
|
||||
8
source/templates/classic/template.mjs
Normal file
8
source/templates/classic/template.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
//Imports
|
||||
import common from "./../common.mjs"
|
||||
|
||||
/** Template processor */
|
||||
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
|
||||
//Common
|
||||
await common(...arguments)
|
||||
}
|
||||
115
source/templates/common.mjs
Normal file
115
source/templates/common.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
/** Template common processor */
|
||||
export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
|
||||
|
||||
//Init
|
||||
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
|
||||
const avatar = imports.imgb64(data.user.avatarUrl)
|
||||
console.debug(`metrics/compute/${login} > formatting common metrics`)
|
||||
|
||||
//Timezone config
|
||||
if (q["config.timezone"]) {
|
||||
const timezone = data.config.timezone = {name:q["config.timezone"], offset:0}
|
||||
try {
|
||||
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset*60*60*1000) || 0
|
||||
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`)
|
||||
} catch {
|
||||
timezone.error = `Failed to use timezone "${timezone.name}"`
|
||||
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
//Plugins
|
||||
for (const name of Object.keys(imports.plugins)) {
|
||||
if (!plugins[name].enabled) {
|
||||
console.log("skipping "+name)
|
||||
continue
|
||||
}
|
||||
|
||||
pending.push((async () => {
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
||||
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries}, plugins[name])
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
|
||||
data.plugins[name] = error
|
||||
}
|
||||
finally {
|
||||
const result = {name, result:data.plugins[name]}
|
||||
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
|
||||
return result
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
//Iterate through user's repositories
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Simple properties with totalCount
|
||||
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_merged", "releases"])
|
||||
computed.repositories[property] += repository[property].totalCount
|
||||
//Forks
|
||||
computed.repositories.forks += repository.forkCount
|
||||
//License
|
||||
if (repository.licenseInfo)
|
||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
|
||||
}
|
||||
|
||||
//Total disk usage
|
||||
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}`
|
||||
|
||||
//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
|
||||
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||
|
||||
//Compute registration date
|
||||
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
||||
const years = Math.floor(diff)
|
||||
const months = Math.floor((diff-years)*12)
|
||||
computed.registration = years ? `${years} year${s(years)} ago` : months ? `${months} month${s(months)} ago` : `${Math.ceil(diff*365)} day${s(Math.ceil(diff*365))} ago`
|
||||
computed.cakeday = [new Date(), new Date(data.user.createdAt)].map(date => date.toISOString().match(/(?<mmdd>\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0])
|
||||
|
||||
//Compute calendar
|
||||
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
||||
|
||||
//Avatar (base64)
|
||||
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
//Token scopes
|
||||
computed.token.scopes = (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
|
||||
|
||||
//Meta
|
||||
data.meta = {version:conf.package.version, author:conf.package.author}
|
||||
|
||||
//Debug flags
|
||||
if ((dflags.includes("--cakeday"))||(q["dflag.cakeday"])) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
|
||||
computed.cakeday = true
|
||||
}
|
||||
if ((dflags.includes("--hireable"))||(q["dflag.hireable"])) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
|
||||
data.user.isHireable = true
|
||||
}
|
||||
if ((dflags.includes("--halloween"))||(q["dflag.halloween"])) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --halloween`)
|
||||
//Haloween color replacer
|
||||
const halloween = content => content
|
||||
.replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph")
|
||||
.replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)")
|
||||
.replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)")
|
||||
.replace(/#30a14e/gi, "var(--color-calendar-halloween-graph-day-L3-bg)")
|
||||
.replace(/#216e39/gi, "var(--color-calendar-halloween-graph-day-L4-bg)")
|
||||
//Update contribution calendar colors
|
||||
computed.calendar.map(day => day.color = halloween(day.color))
|
||||
//Update isocalendar colors
|
||||
const waiting = [...pending]
|
||||
pending.push((async () => {
|
||||
await Promise.all(waiting)
|
||||
if (data.plugins.isocalendar?.svg)
|
||||
data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg)
|
||||
return {name:"dflag.halloween", result:true}
|
||||
})())
|
||||
}
|
||||
}
|
||||
221
source/templates/repository/image.svg
Normal file
221
source/templates/repository/image.svg
Normal file
@@ -0,0 +1,221 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 0
|
||||
+ (!!base.header)*42
|
||||
+ (!!plugins.traffic)*18
|
||||
+ (!!plugins.followup)*68
|
||||
+ (!!base.metadata)*28
|
||||
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
|
||||
+ Math.max(0, ((!!base.header)+(!!base.metadata)+(!!plugins.followup)+(!!plugins.projects))-1)*4
|
||||
%>">
|
||||
|
||||
<defs><style><%= fonts %></style></defs>
|
||||
|
||||
<style>
|
||||
<%= style %>
|
||||
</style>
|
||||
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<% if (errors.length) { %>
|
||||
<section>
|
||||
<div class="row">
|
||||
<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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= errors.map(({error}) => error.message).join(", ") %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
|
||||
<% if (base.header) { %>
|
||||
<section>
|
||||
<div class="row">
|
||||
<section>
|
||||
<div class="field <%= computed.cakeday ? 'cakeday' : '' %>">
|
||||
<% if (computed.cakeday) { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 1.5a1.25 1.25 0 100 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707zM2 2.75c0 .45.108.875.3 1.25h-.55A1.75 1.75 0 000 5.75v2c0 .698.409 1.3 1 1.582v4.918c0 .966.784 1.75 1.75 1.75h10.5A1.75 1.75 0 0015 14.25V9.332c.591-.281 1-.884 1-1.582v-2A1.75 1.75 0 0014.25 4h-.55a2.75 2.75 0 00-2.45-4c-.984 0-1.874.42-2.57 1.23A5.086 5.086 0 008 2.274a5.086 5.086 0 00-.68-1.042C6.623.42 5.733 0 4.75 0A2.75 2.75 0 002 2.75zM8.941 4h2.309a1.25 1.25 0 100-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793zm-1.84 1.5H1.75a.25.25 0 00-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-.149zm1.649 0V8h5.5a.25.25 0 00.25-.25v-2a.25.25 0 00-.25-.25h-5.5zm0 4h4.75v4.75a.25.25 0 01-.25.25h-4.5v-5zm-1.5 0v5h-4.5a.25.25 0 01-.25-.25V9.5h4.75z"></path></svg>
|
||||
Created <%= computed.registration %>
|
||||
<% } else { %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
|
||||
Created <%= computed.registration %>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" d="M2.5 3.5c0-.133.058-.318.282-.55.227-.237.592-.484 1.1-.708C4.899 1.795 6.354 1.5 8 1.5c1.647 0 3.102.295 4.117.742.51.224.874.47 1.101.707.224.233.282.418.282.551 0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 5.205 9.646 5.5 8 5.5c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707-.224-.233-.282-.418-.282-.551zM1 3.5c0-.626.292-1.165.7-1.59.406-.422.956-.767 1.579-1.041C4.525.32 6.195 0 8 0c1.805 0 3.475.32 4.722.869.622.274 1.172.62 1.578 1.04.408.426.7.965.7 1.591v9c0 .626-.292 1.165-.7 1.59-.406.422-.956.767-1.579 1.041C11.476 15.68 9.806 16 8 16c-1.805 0-3.475-.32-4.721-.869-.623-.274-1.173-.62-1.579-1.04-.408-.426-.7-.965-.7-1.591v-9zM2.5 8V5.724c.241.15.503.286.779.407C4.525 6.68 6.195 7 8 7c1.805 0 3.475-.32 4.722-.869.275-.121.537-.257.778-.407V8c0 .133-.058.318-.282.55-.227.237-.592.484-1.1.708C11.101 9.705 9.646 10 8 10c-1.647 0-3.102-.295-4.117-.742-.51-.224-.874-.47-1.101-.707C2.558 8.318 2.5 8.133 2.5 8zm0 2.225V12.5c0 .133.058.318.282.55.227.237.592.484 1.1.708 1.016.447 2.471.742 4.118.742 1.647 0 3.102-.295 4.117-.742.51-.224.874-.47 1.101-.707.224-.233.282-.418.282-.551v-2.275c-.241.15-.503.285-.778.406-1.247.549-2.917.869-4.722.869-1.805 0-3.475-.32-4.721-.869a6.236 6.236 0 01-.779-.406z"></path></svg>
|
||||
<%= computed.diskUsage %> used
|
||||
</div>
|
||||
<% if (plugins.traffic) { %>
|
||||
<div class="field <%= plugins.traffic.error ? 'error' : '' %>">
|
||||
<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>
|
||||
<% if (plugins.traffic.error) { %>
|
||||
<%= plugins.traffic.error.message %>
|
||||
<% } else { %>
|
||||
<%= plugins.traffic.views.count %> view<%= s(plugins.traffic.views.count) %> in last two weeks
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
<section>
|
||||
<div class="field calendar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 <%= computed.calendar.length*15 %> 11" width="<%= computed.calendar.length*15 %>" height="16">
|
||||
<g>
|
||||
<% for (const [x, {color}] of Object.entries(computed.calendar)) { %>
|
||||
<rect class="day" x="<%= x*15 %>" y="0" width="11" height="11" fill="<%= color %>" rx="2" ry="2" />
|
||||
<% } %>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<% if (plugins.lines) { %>
|
||||
<div class="field <%= plugins.lines.error ? 'error' : '' %>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V4.664a.25.25 0 00-.073-.177l-2.914-2.914a.25.25 0 00-.177-.073H2.75zM1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16H2.75A1.75 1.75 0 011 14.25V1.75zm7 1.5a.75.75 0 01.75.75v1.5h1.5a.75.75 0 010 1.5h-1.5v1.5a.75.75 0 01-1.5 0V7h-1.5a.75.75 0 010-1.5h1.5V4A.75.75 0 018 3.25zm-3 8a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5h-4.5a.75.75 0 01-.75-.75z"></path></svg>
|
||||
<% if (plugins.lines.error) { %>
|
||||
<%= plugins.lines.error.message %>
|
||||
<% } else { %>
|
||||
<%= plugins.lines.added %> added, <%= plugins.lines.deleted %> removed
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.followup) { %>
|
||||
<div class="row">
|
||||
|
||||
<section class="column">
|
||||
<h3>Issues</h3>
|
||||
<% if (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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.followup.error.message %>
|
||||
</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="<%= plugins.followup.issues.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#issues-bar)" x="0" y="0" width="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" height="8" fill="#d73a49"/>
|
||||
<rect mask="url(#issues-bar)" x="<%= (plugins.followup.issues.closed/plugins.followup.issues.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.issues.closed/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"><%= 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"><%= plugins.followup.issues.open %> Open</span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
|
||||
<section class="column">
|
||||
<h3>Pull requests</h3>
|
||||
<% if (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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.followup.error.message %>
|
||||
</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="<%= plugins.followup.pr.count ? 0 : 220 %>" height="8" fill="#d1d5da"/>
|
||||
<rect mask="url(#pr-bar)" x="0" y="0" width="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" height="8" fill="#6f42c1"/>
|
||||
<rect mask="url(#pr-bar)" x="<%= (plugins.followup.pr.merged/plugins.followup.pr.count)*220 || 0 %>" y="0" width="<%= (1-plugins.followup.pr.merged/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"><%= 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"><%= plugins.followup.pr.open %> Open</span>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (plugins.projects) { %>
|
||||
<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="M1.75 0A1.75 1.75 0 000 1.75v12.5C0 15.216.784 16 1.75 16h12.5A1.75 1.75 0 0016 14.25V1.75A1.75 1.75 0 0014.25 0H1.75zM1.5 1.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v12.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V1.75zM11.75 3a.75.75 0 00-.75.75v7.5a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75zm-8.25.75a.75.75 0 011.5 0v5.5a.75.75 0 01-1.5 0v-5.5zM8 3a.75.75 0 00-.75.75v3.5a.75.75 0 001.5 0v-3.5A.75.75 0 008 3z"></path></svg>
|
||||
Active projects
|
||||
</h2>
|
||||
<div class="row">
|
||||
<% if (plugins.projects.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="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.projects.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
<% } else { %>
|
||||
<section>
|
||||
<% for (const {name, updated, progress} of plugins.projects.list) { %>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<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 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
|
||||
<%= name %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="row">
|
||||
<section>
|
||||
<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="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.5 4.75a.75.75 0 00-1.5 0v3.5a.75.75 0 00.471.696l2.5 1a.75.75 0 00.557-1.392L8.5 7.742V4.75z"></path></svg>
|
||||
Updated <%= updated %>
|
||||
</div>
|
||||
</section>
|
||||
<% if (progress.enabled) { %>
|
||||
<section>
|
||||
<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>
|
||||
<%= [progress.done ? `${progress.done} done` : "", progress.doing ? `${progress.doing} doing` : "", progress.todo ? `${progress.todo} todo` : ""].filter(str => str).join(" · ") %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<% if (progress.enabled) { %>
|
||||
<div class="field center horizontal-wrap ">
|
||||
<svg class="bar" xmlns="http://www.w3.org/2000/svg" width="460" height="8">
|
||||
<mask id="project-bar">
|
||||
<rect x="0" y="0" width="460" height="8" fill="white" rx="5"/>
|
||||
</mask>
|
||||
<rect mask="url(#project-bar)" x="0" y="0" width="<%= (progress.done/progress.total)*460 %>" height="8" fill="#28A745"/>
|
||||
<rect mask="url(#project-bar)" x="<%= (progress.done/progress.total)*460 %>" y="0" width="<%= (progress.doing/progress.total)*460 %>" height="8" fill="#6F42C1"/>
|
||||
<rect mask="url(#project-bar)" x="<%= ((progress.done+progress.doing)/progress.total)*460 %>" y="0" width="<%= (progress.todo/progress.total)*460 %>" height="8" fill="#d1d5da"/>
|
||||
</svg>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% } %>
|
||||
|
||||
<% if (base.metadata) { %>
|
||||
<footer>
|
||||
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
|
||||
</footer>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 18 KiB |
62
source/templates/repository/template.mjs
Normal file
62
source/templates/repository/template.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
//Imports
|
||||
import common from "./../common.mjs"
|
||||
|
||||
/** Template processor */
|
||||
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
|
||||
//Check arguments
|
||||
const {repo} = q
|
||||
if (!repo) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`)
|
||||
data.errors.push({error:{message:`You must pass a "repo" argument to use this template`}})
|
||||
return await common(...arguments)
|
||||
}
|
||||
|
||||
//Retrieving single repository
|
||||
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
|
||||
const {user:{repository}} = await graphql(queries.repository({login, repo}))
|
||||
data.user.repositories.nodes = [repository]
|
||||
|
||||
//Get commit activity
|
||||
console.debug(`metrics/compute/${login}/${repo} > querying api for commits`)
|
||||
const commits = []
|
||||
for (let page = 0; page < 100; page++) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
|
||||
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page})
|
||||
if (!data.length) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
|
||||
break
|
||||
}
|
||||
commits.push(...data)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/${repo} > ${commits.length} commits loaded`)
|
||||
|
||||
//Override creation date and disk usage
|
||||
data.user.createdAt = repository.createdAt
|
||||
data.user.repositories.totalDiskUsage = repository.diskUsage
|
||||
|
||||
//Override contributions calendar
|
||||
const days = 14
|
||||
//Compute relative date for each contribution
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const contributions = commits.map(({commit}) => Math.abs(Math.ceil((now - new Date(commit.committer.date))/(24*60*60*1000))))
|
||||
//Count contributions per relative day
|
||||
const calendar = new Array(days).fill(0)
|
||||
for (const day of contributions)
|
||||
calendar[day]++
|
||||
calendar.splice(days)
|
||||
const max = Math.max(...calendar)
|
||||
//Override contributions calendar
|
||||
data.user.calendar.contributionCalendar.weeks = calendar.map(commit => ({contributionDays:{color:commit ? `var(--color-calendar-graph-day-L${Math.ceil(commit/max/0.25)}-bg)` : "var(--color-calendar-graph-day-bg)"}}))
|
||||
|
||||
//Override plugins parameters
|
||||
q["projects.limit"] = 0
|
||||
|
||||
//Common
|
||||
await common(...arguments)
|
||||
await Promise.all(pending)
|
||||
|
||||
//Reformat projects names
|
||||
if (data.plugins.projects)
|
||||
data.plugins.projects.list?.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim())
|
||||
}
|
||||
15
source/templates/terminal/fonts.css
Normal file
15
source/templates/terminal/fonts.css
Normal file
File diff suppressed because one or more lines are too long
180
source/templates/terminal/image.svg
Normal file
180
source/templates/terminal/image.svg
Normal file
@@ -0,0 +1,180 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="480" height="<%= 48
|
||||
+ (!!base.header)*62
|
||||
+ (!!base.metadata)*108
|
||||
+ (!!base.activity)*108
|
||||
+ (!!base.community)*94
|
||||
+ (!!base.repositories)*142
|
||||
+ ((!!base.repositories)*(!!plugins.traffic))*18
|
||||
+ ((!!base.repositories)*(!!plugins.followup))*102
|
||||
+ ((!!base.repositories)*(!!plugins.lines))*34
|
||||
+ (!!plugins.pagespeed)*110 + (plugins.pagespeed?.detailed ?? 0)*6*16
|
||||
+ (!!plugins.languages)*124
|
||||
+ (!!plugins.gists)*58
|
||||
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.gists))-1))*20
|
||||
%>">
|
||||
<%
|
||||
meta.$ = `<span class="ps1-path">${`${user.login}`.toLocaleLowerCase()}@metrics</span>:<span class="ps1-location">~</span>${computed.token.scopes.includes("repo") ? "#" : "$"}`
|
||||
meta.animations = !meta.placeholder ? {stdin:.16, stdout:.28, length:(2+Object.keys(base).length+Object.keys(plugins).length)} : {stdin:0, stdout:0, length:0}
|
||||
%>
|
||||
|
||||
<defs><style><%= fonts %></style></defs>
|
||||
|
||||
<style>
|
||||
<%= style %>
|
||||
.stdin, .stdout {
|
||||
animation-duration: <%= meta.animations.stdin %>s;
|
||||
}
|
||||
.stdout {
|
||||
animation-duration: <%= meta.animations.stdout %>s;
|
||||
}
|
||||
<% for (let i = 0, d = 0; i < meta.animations.length; i++, d+=meta.animations.stdin+meta.animations.stdout) { %>
|
||||
.stdin:nth-of-type(<%= i+1 %>) {
|
||||
animation-delay: <%= d %>s;
|
||||
}
|
||||
.stdout:nth-of-type(<%= i+2 %>) {
|
||||
animation-delay: <%= d+meta.animations.stdin %>s;
|
||||
}
|
||||
<% if (i === meta.animations.length-1) { %>
|
||||
footer {
|
||||
animation-delay: <%= d %>s;
|
||||
}
|
||||
<% } %>
|
||||
<% } %>
|
||||
</style>
|
||||
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<div class="header">
|
||||
<span class="title"></span>
|
||||
<div class="buttons">
|
||||
<div class="button">─</div>
|
||||
<div class="button">□</div>
|
||||
<div class="button exit">✕</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre><%# -%>
|
||||
<% if (base.metadata) { %>
|
||||
<div class="banner"><%# -%>
|
||||
GitHub metrics generator <%= meta.version %>
|
||||
These generated metrics comes with ABSOLUTELY NO
|
||||
WARRANTY, to the extent permitted by applicable law.
|
||||
|
||||
Last generated: <%= new Date().toGMTString() %>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (base.header) { %>
|
||||
<div class="stdin"><%- meta.$ %> whoami</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
<b><%= user.name || user.login %></b> registered=<%= computed.registration.match(/^.+? [ym]/)[0].replace(/ /g, "") %>, uid=<%= `${user.databaseId}`.substr(-4) %>, gid=<%= user.organizations.totalCount %>
|
||||
contributed to <%= user.repositoriesContributedTo.totalCount %> repositor<%= s(user.repositoriesContributedTo.totalCount, "y") %> <b><% for (const [x, {color}] of Object.entries(computed.calendar)) { -%><span style="color:<%= color %>">#</span><% } %></b>
|
||||
followed by <b><%= user.followers.totalCount %></b> user<%= s(user.followers.totalCount) %>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if ((base.activity)||(base.community)) { %>
|
||||
<div class="stdin"><%- meta.$ %> git status</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
<% if (base.activity) { -%>
|
||||
<b>Recent activity</b>
|
||||
<b><%= `${computed.commits}`.padStart(5) %></b> commit<%= s(computed.commits) %>
|
||||
<b><%= `${user.contributionsCollection.totalPullRequestReviewContributions}`.padStart(5) %></b> pull request<%= s(user.contributionsCollection.totalPullRequestReviewContributions) %> reviewed
|
||||
<b><%= `${user.contributionsCollection.totalPullRequestContributions}`.padStart(5) %></b> pull request<%= s(user.contributionsCollection.totalPullRequestContributions) %> opened
|
||||
<b><%= `${user.contributionsCollection.totalIssueContributions}`.padStart(5) %></b> issue<%= s(user.contributionsCollection.totalIssueContributions) %> opened
|
||||
<b><%= `${user.issueComments.totalCount}`.padStart(5) %></b> issue comment<%= s(user.issueComments.totalCount) %>
|
||||
<% } -%>
|
||||
<% if ((base.activity)&&(base.community)) { -%>
|
||||
|
||||
<% } -%>
|
||||
<% if (base.community) { -%>
|
||||
<b>Tracked activity</b>
|
||||
<b><%= `${user.following.totalCount}`.padStart(5) %></b> user<%= s(user.followers.totalCount) %> followed
|
||||
<b><%= `${user.sponsorshipsAsSponsor.totalCount}`.padStart(5) %></b> repositor<%= s(user.sponsorshipsAsSponsor.totalCount, "y") %> sponsored
|
||||
<b><%= `${user.starredRepositories.totalCount}`.padStart(5) %></b> repositor<%= s(user.starredRepositories.totalCount, "y") %> starred
|
||||
<b><%= `${user.watching.totalCount}`.padStart(5) %></b> repositor<%= s(user.watching.totalCount, "y") %> watched
|
||||
<% } -%>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (base.repositories) { %>
|
||||
<div class="stdin"><%- meta.$ %> ls -lh github/repositories</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
Total <%= user.repositories.totalCount %> repositor<%= s(user.repositories.totalCount, "y") %> - <%= computed.diskUsage %>
|
||||
<% if (plugins.traffic) { if (plugins.traffic.error) { -%>
|
||||
---- <b> </b> views <span class="error">(<%= plugins.traffic.error.message %>)</span>
|
||||
<% } else { -%>
|
||||
-r-- <b><%= `${plugins.traffic.views.count}`.padStart(5) %></b> views
|
||||
<% }} -%>
|
||||
-r-- <b><%= `${computed.repositories.stargazers}`.padStart(5) %></b> stargazer<%= s(computed.repositories.stargazers) %>
|
||||
-r-- <b><%= `${computed.repositories.forks}`.padStart(5) %></b> fork<%= s(computed.repositories.forks) %>
|
||||
-r-- <b><%= `${computed.repositories.watchers}`.padStart(5) %></b> watcher<%= s(computed.repositories.watchers) %>
|
||||
dr-x <b><%= `${user.packages.totalCount}`.padStart(5) %></b> package<%= s(user.packages.totalCount) %>
|
||||
<% if (plugins.followup) { if (plugins.followup.error) { -%>
|
||||
d--- <b> </b> ISSUES <span class="error">(<%= plugins.followup.error.message %>)</span>
|
||||
d--- <b> </b> PULL_REQUESTS <span class="error">(<%= plugins.followup.error.message %>)</span>
|
||||
<% } else { -%>
|
||||
dr-x <b><%= `${plugins.followup.issues.count}`.padStart(5) %></b> ISSUES
|
||||
-r-- <b><%= `${plugins.followup.issues.open}`.padStart(5) %></b> ├── open
|
||||
-r-- <b><%= `${plugins.followup.issues.closed}`.padStart(5) %></b> └── closed
|
||||
dr-x <b><%= `${plugins.followup.issues.count}`.padStart(5) %></b> PULL_REQUESTS
|
||||
-r-- <b><%= `${plugins.followup.pr.open}`.padStart(5) %></b> ├── open
|
||||
-r-- <b><%= `${plugins.followup.pr.merged}`.padStart(5) %></b> └── merged
|
||||
<% }} -%>
|
||||
<% if (computed.licenses.favorite.length) { -%>
|
||||
dr-x LICENSE
|
||||
-r-- └── <%= computed.licenses.favorite %>
|
||||
<% } -%>
|
||||
<% if (plugins.lines) { if (plugins.lines.error) { %>
|
||||
<span class="diff error">@@ <%= plugins.lines.error.message %> @@</span><% } else { %>
|
||||
<span class="diff">@@ -<%= plugins.lines.deleted %> +<%= plugins.lines.added %> @@</span>
|
||||
<% }} -%>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (plugins.gists) { %>
|
||||
<div class="stdin"><%- meta.$ %> ls -lh github/gists</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
Total <%= plugins.gists.totalCount %> gist<%= s(plugins.gists.totalCount) %>
|
||||
-r-- <b><%= `${plugins.gists.stargazers}`.padStart(5) %></b> stargazer<%= s(plugins.gists.stargazers) %>
|
||||
-r-- <b><%= `${plugins.gists.forks}`.padStart(5) %></b> fork<%= s(plugins.gists.forks) %>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (plugins.languages) { %>
|
||||
<div class="stdin"><%- meta.$ %> locale</div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
<% if (plugins.languages.error) { -%>
|
||||
<span class="error"><%= plugins.languages.error.message %></span><%# -%>
|
||||
<% } else { for (const {name, value} of plugins.languages.favorites) { -%>
|
||||
<b><%= name.toLocaleUpperCase().padEnd(12) %></b> [<%= "#".repeat(Math.ceil(100*value/5)).padEnd(20) %>] <%= (100*value).toFixed(2).padEnd(5) %>%
|
||||
<% }} -%>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (plugins.pagespeed) { %>
|
||||
<div class="stdin"><%- meta.$ %> curl -<%= plugins.pagespeed.detailed ? "v" : "" %>I <%= user.websiteUrl %></div><%# -%>
|
||||
<div class="stdout"><%# -%>
|
||||
<% if (plugins.pagespeed.error) { -%>
|
||||
<span class="error"><%= plugins.pagespeed.error.message %></span><% } else { -%>
|
||||
<b>User-Agent</b>: Google PageSpeed API
|
||||
<b>Location</b>: <%= user.websiteUrl %>
|
||||
<% for (const {score, title} of plugins.pagespeed.scores) { -%>
|
||||
<b><%= `X-${title.replace(/ /g, "-")}` %></b>: <%= !Number.isNaN(score) ? Math.round(score*100) : "-" %>%
|
||||
<% } -%>
|
||||
<% if (plugins.pagespeed.detailed) { for (const {name, score, suffix = "", threshold} of [
|
||||
{name:"Time to interactive", score:plugins.pagespeed.metrics.interactive/1000, suffix:"s", threshold:[3.785, 7.3]},
|
||||
{name:"Speed Index", score:plugins.pagespeed.metrics.speedIndex/1000, suffix:"s", threshold:[3.387, 5.8]},
|
||||
{name:"Total Blocking Time", score:plugins.pagespeed.metrics.totalBlockingTime/1000, suffix:"s", threshold:[.287, .6]},
|
||||
{name:"First Contentful Paint", score:plugins.pagespeed.metrics.firstContentfulPaint/1000, suffix:"s", threshold:[2.336, 4]},
|
||||
{name:"Largest Contentful Paint", score:plugins.pagespeed.metrics.largestContentfulPaint/1000, suffix:"s", threshold:[2.5, 4]},
|
||||
{name:"Cumulative Layout Shift", score:+plugins.pagespeed.metrics.cumulativeLayoutShift, threshold:[.1, .25]}
|
||||
]) { -%>
|
||||
<b>> <%= name %></b>: <%= !Number.isNaN(score) ? score : "-" %><%= suffix %> <% if (Number.isNaN(score)) { %><% } else if (score <= threshold[0]) { %>(ok +)<% } else if (score <= threshold[1]) { %>(ok)<% } else { %>(bad)<% } %>
|
||||
<% }}} -%>
|
||||
</div><% } -%>
|
||||
<%# ============================================================= -%>
|
||||
<% if (base.metadata) { -%>
|
||||
|
||||
<footer>Connection reset by <%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %>.<%= Math.floor(256*Math.random()) %></footer><%# -%>
|
||||
<% } -%></pre>
|
||||
|
||||
</div>
|
||||
</foreignObject>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 9.8 KiB |
127
source/templates/terminal/style.css
Normal file
127
source/templates/terminal/style.css
Normal file
@@ -0,0 +1,127 @@
|
||||
/* SVG global context */
|
||||
svg {
|
||||
font-family: 'Courier Prime';
|
||||
font-size: 14px;
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
/* Title bar */
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background: linear-gradient(#504b45 0%,#3c3b37 100%);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #d5d0ce;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
font-size: 8px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 100%;
|
||||
background: linear-gradient(#7d7871 0%, #595953 100%);
|
||||
text-shadow: 0px 1px 0px rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.button.exit {
|
||||
background: linear-gradient(#f37458 0%, #de4c12 100%);
|
||||
}
|
||||
|
||||
/* Terminal */
|
||||
pre, .banner, footer {
|
||||
margin: 0;
|
||||
font-family: 'Courier Prime';
|
||||
color: #DDDDDD;
|
||||
}
|
||||
pre {
|
||||
background: #42092B;
|
||||
padding: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.banner, footer {
|
||||
color: #AE9DA7;
|
||||
}
|
||||
|
||||
/* Prompt */
|
||||
.ps1-path {
|
||||
color: #7EDA29;
|
||||
}
|
||||
|
||||
.ps1-location {
|
||||
color: #4878c0;
|
||||
}
|
||||
|
||||
/* Diff */
|
||||
.diff {
|
||||
color: #3A96DD;
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.error {
|
||||
color: #cb2431;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.stdin, footer {
|
||||
width: 0%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
animation-name: stdin-animation;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.stdout {
|
||||
max-height: 0%;
|
||||
overflow: hidden;
|
||||
animation-name: stdout-animation;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes stdin-animation {
|
||||
0% { width: 0%; }
|
||||
100% { width: 100%; }
|
||||
}
|
||||
|
||||
@keyframes stdout-animation {
|
||||
0% { max-height: 0; }
|
||||
100% { max-height: 360px; }
|
||||
}
|
||||
|
||||
/* Calendar */
|
||||
:root {
|
||||
--color-calendar-graph-day-bg: #ebedf0;
|
||||
--color-calendar-graph-day-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-bg: #9be9a8;
|
||||
--color-calendar-graph-day-L2-bg: #40c463;
|
||||
--color-calendar-graph-day-L3-bg: #30a14e;
|
||||
--color-calendar-graph-day-L4-bg: #216e39;
|
||||
--color-calendar-halloween-graph-day-L1-bg: #ffee4a;
|
||||
--color-calendar-halloween-graph-day-L2-bg: #ffc501;
|
||||
--color-calendar-halloween-graph-day-L3-bg: #fe9600;
|
||||
--color-calendar-halloween-graph-day-L4-bg: #03001c;
|
||||
--color-calendar-graph-day-L4-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L3-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L2-border: rgba(27,31,35,0.06);
|
||||
--color-calendar-graph-day-L1-border: rgba(27,31,35,0.06);
|
||||
}
|
||||
10
source/templates/terminal/template.mjs
Normal file
10
source/templates/terminal/template.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
//Imports
|
||||
import common from "./../common.mjs"
|
||||
|
||||
/** Template processor */
|
||||
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
|
||||
//Common
|
||||
await common(...arguments)
|
||||
//Disable optimization to keep white-spaces
|
||||
q.raw = true
|
||||
}
|
||||
Reference in New Issue
Block a user