Refactor source paths

This commit is contained in:
linguist
2020-12-30 20:05:31 +01:00
parent dd0b3c5626
commit e38614a100
45 changed files with 32 additions and 36 deletions

View 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 metrics](${this.url})`
},
//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)
}
},
})
})()

View 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>

View 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;
}