diff --git a/source/app/mocks/api/github/graphql/discussions.categories.mjs b/source/app/mocks/api/github/graphql/discussions.categories.mjs
new file mode 100644
index 00000000..e5fa8fdc
--- /dev/null
+++ b/source/app/mocks/api/github/graphql/discussions.categories.mjs
@@ -0,0 +1,27 @@
+/**Mocked data */
+export default function({faker, query, login = faker.internet.userName()}) {
+ console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
+ return /after: "MOCKED_CURSOR"/m.test(query)
+ ? ({
+ user:{
+ repositoryDiscussions:{
+ edges:[],
+ nodes:[],
+ }
+ }
+ })
+ : ({
+ user:{
+ repositoryDiscussions:{
+ edges:new Array(100).fill(null).map(_ => ({cursor:"MOCKED_CURSOR"})),
+ nodes:new Array(100).fill(null).map(_ => ({
+ category:{
+ emoji:faker.random.arrayElement([":chart_with_upwards_trend:", ":chart_with_downwards_trend:", ":bar_char:"]),
+ name:faker.lorem.slug()
+ }
+ }))
+ }
+ }
+ })
+}
+
diff --git a/source/app/mocks/api/github/graphql/discussions.statistics.mjs b/source/app/mocks/api/github/graphql/discussions.statistics.mjs
new file mode 100644
index 00000000..b899f7ea
--- /dev/null
+++ b/source/app/mocks/api/github/graphql/discussions.statistics.mjs
@@ -0,0 +1,11 @@
+/**Mocked data */
+export default function({faker, query, login = faker.internet.userName()}) {
+ console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
+ return ({
+ user:{
+ started:{totalCount:faker.datatype.number(1000)},
+ comments:{totalCount:faker.datatype.number(1000)},
+ answers:{totalCount:faker.datatype.number(1000)}
+ }
+ })
+}
\ No newline at end of file
diff --git a/source/app/mocks/api/github/graphql/repositories.repository.mjs b/source/app/mocks/api/github/graphql/repositories.repository.mjs
new file mode 100644
index 00000000..488dd422
--- /dev/null
+++ b/source/app/mocks/api/github/graphql/repositories.repository.mjs
@@ -0,0 +1,29 @@
+/**Mocked data */
+export default function({faker, query, login = faker.internet.userName()}) {
+ console.debug("metrics/compute/mocks > mocking graphql api result > stars/default")
+ return ({
+ repository:{
+ createdAt: faker.date.past(),
+ description:"đ An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
+ forkCount:faker.datatype.number(100),
+ isFork:false,
+ issues:{
+ totalCount:faker.datatype.number(100),
+ },
+ nameWithOwner:"lowlighter/metrics",
+ openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a",
+ pullRequests:{
+ totalCount:faker.datatype.number(100),
+ },
+ stargazerCount:faker.datatype.number(10000),
+ licenseInfo:{
+ nickname:null,
+ name:"MIT License",
+ },
+ primaryLanguage:{
+ color:"#f1e05a",
+ name:"JavaScript",
+ },
+ },
+ })
+}
diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js
index 63d4cc51..aa3761ec 100644
--- a/source/app/web/statics/app.placeholder.js
+++ b/source/app/web/statics/app.placeholder.js
@@ -586,6 +586,20 @@
},
})
: null),
+ //Discussions
+ ...(set.plugins.enabled.discussions
+ ? ({
+ discussions: {
+ categories: {
+ stats: { 'đ Q&A': faker.datatype.number(100), 'đŖ Announcements': faker.datatype.number(100), 'đĄ Ideas': faker.datatype.number(100), 'đŦ General': faker.datatype.number(100) },
+ favorite: 'đŖ Announcements'
+ },
+ started: faker.datatype.number(1000),
+ comments: faker.datatype.number(1000),
+ answers: faker.datatype.number(1000),
+ },
+ })
+ : null),
//Posts
...(set.plugins.enabled.posts
? ({
@@ -664,7 +678,31 @@
},
})
: null),
- //Stars
+ //Repositories
+ ...(set.plugins.enabled.repositories
+ ? ({
+ repositories: {
+ list: new Array(Number(options["repositories.featured"].split(",").length) - 1).fill(null).map((_, i) => ({
+ created: faker.date.past(),
+ description: faker.lorem.sentence(),
+ forkCount: faker.datatype.number(100),
+ isFork: faker.datatype.boolean(),
+ issues: {
+ totalCount: faker.datatype.number(100),
+ },
+ nameWithOwner: `${faker.random.word()}/${faker.random.word()}`,
+ openGraphImageUrl: faker.internet.url(),
+ pullRequests: {
+ totalCount: faker.datatype.number(100),
+ },
+ stargazerCount: faker.datatype.number(10000),
+ licenseInfo: { nickname: null, name: "License" },
+ primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() },
+ })),
+ },
+ })
+ : null),
+ //Stargazers
...(set.plugins.enabled.stargazers
? ({
get stargazers() {
diff --git a/source/plugins/discussions/README.md b/source/plugins/discussions/README.md
new file mode 100644
index 00000000..23bae3c4
--- /dev/null
+++ b/source/plugins/discussions/README.md
@@ -0,0 +1,21 @@
+### đŦ Discussions
+
+The *discussions* plugin displays your GitHub discussions metrics.
+
+
+
+
+
+ |
+
+
+#### âšī¸ Examples workflows
+
+[âĄī¸ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_discussions: yes
+```
\ No newline at end of file
diff --git a/source/plugins/discussions/index.mjs b/source/plugins/discussions/index.mjs
new file mode 100644
index 00000000..8327fb68
--- /dev/null
+++ b/source/plugins/discussions/index.mjs
@@ -0,0 +1,46 @@
+//Setup
+ export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
+ //Plugin execution
+ try {
+ //Check if plugin is enabled and requirements are met
+ if ((!enabled)||(!q.discussions))
+ return null
+
+ //Load inputs
+ imports.metadata.plugins.discussions.inputs({data, account, q})
+ const discussions = {categories:{}}
+
+ //Fetch general statistics
+ const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount]))
+ Object.assign(discussions, stats)
+
+ //Load started discussions
+ {
+ const fetched = []
+ const categories = {}
+ let cursor = null
+ let pushed = 0
+ do {
+ console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`)
+ const {user:{repositoryDiscussions:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after:cursor ? `after: "${cursor}"` : ""}))
+ cursor = edges?.[edges?.length - 1]?.cursor
+ fetched.push(...nodes)
+ pushed = nodes.length
+ console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} discussions after ${cursor}`)
+ } while ((pushed) && (cursor))
+
+ //Compute favorite category
+ for (const category of [...fetched.map(({category:{emoji, name}}) => `${imports.emoji.get(emoji)} ${name}`)])
+ categories[category] = (categories[category] ?? 0) + 1
+ discussions.categories.stats = categories
+ discussions.categories.favorite = Object.entries(categories).sort((a, b) => b[1] - a[1]).map(([name]) => name).shift() ?? null
+ }
+
+ //Results
+ return discussions
+ }
+ //Handle errors
+ catch (error) {
+ throw {error:{message:"An error occured", instance:error}}
+ }
+ }
diff --git a/source/plugins/discussions/metadata.yml b/source/plugins/discussions/metadata.yml
new file mode 100644
index 00000000..84058a96
--- /dev/null
+++ b/source/plugins/discussions/metadata.yml
@@ -0,0 +1,12 @@
+name: "đŦ Discussions"
+cost: 1 GraphQL request + 1 GraphQL request per 100 discussions started
+category: github
+supports:
+ - user
+inputs:
+
+ # Enable or disable plugin
+ plugin_discussions:
+ description: GitHub discussions metrics
+ type: boolean
+ default: no
\ No newline at end of file
diff --git a/source/plugins/discussions/queries/categories.graphql b/source/plugins/discussions/queries/categories.graphql
new file mode 100644
index 00000000..cc6ba0f6
--- /dev/null
+++ b/source/plugins/discussions/queries/categories.graphql
@@ -0,0 +1,15 @@
+query DiscussionsCategories {
+ user(login: "$login") {
+ repositoryDiscussions($after first: 100, orderBy: {field: CREATED_AT, direction: DESC}) {
+ edges {
+ cursor
+ }
+ nodes {
+ category {
+ emoji
+ name
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/plugins/discussions/queries/statistics.graphql b/source/plugins/discussions/queries/statistics.graphql
new file mode 100644
index 00000000..281bf769
--- /dev/null
+++ b/source/plugins/discussions/queries/statistics.graphql
@@ -0,0 +1,13 @@
+query DiscussionsStatistics {
+ user(login: "$login") {
+ started: repositoryDiscussions {
+ totalCount
+ }
+ comments: repositoryDiscussionComments {
+ totalCount
+ }
+ answers: repositoryDiscussionComments(onlyAnswers: true) {
+ totalCount
+ }
+ }
+}
\ No newline at end of file
diff --git a/source/plugins/discussions/tests.yml b/source/plugins/discussions/tests.yml
new file mode 100644
index 00000000..d1a46e9a
--- /dev/null
+++ b/source/plugins/discussions/tests.yml
@@ -0,0 +1,5 @@
+- name: Discussions plugin (default)
+ uses: lowlighter/metrics@latest
+ with:
+ token: MOCKED_TOKEN
+ plugin_discussions: yes
\ No newline at end of file
diff --git a/source/plugins/repositories/README.md b/source/plugins/repositories/README.md
new file mode 100644
index 00000000..826f64d9
--- /dev/null
+++ b/source/plugins/repositories/README.md
@@ -0,0 +1,26 @@
+### đ Repositories
+
+The *repositories* plugin can display a list of chosen featured repositories.
+
+
+
+
+
+ |
+
+
+It is mostly intended for external usage as [pinned repositories](https://www.google.com/search?client=firefox-b-d&q=github+pinned+repositories) is probably a better alternative if you want to embed them on your profile.
+
+Because of limitations of using SVG inside of `
` tags, people won't be able to click on it.
+
+#### âšī¸ Examples workflows
+
+[âĄī¸ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_repositories: yes
+ plugin_repositories_list: lowlighter/metrics, denoland/deno # List of repositories you want to feature
+```
\ No newline at end of file
diff --git a/source/plugins/repositories/index.mjs b/source/plugins/repositories/index.mjs
new file mode 100644
index 00000000..163bdd46
--- /dev/null
+++ b/source/plugins/repositories/index.mjs
@@ -0,0 +1,38 @@
+//Setup
+export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
+ //Plugin execution
+ try {
+ //Check if plugin is enabled and requirements are met
+ if ((!enabled)||(!q.repositories))
+ return null
+
+ //Load inputs
+ let {featured} = imports.metadata.plugins.repositories.inputs({data, account, q})
+
+ //Initialization
+ const repositories = {list:[]}
+
+ //Fetch repositories informations
+ for (const repo of featured) {
+ const {owner = login, name} = repo.match(/^(?:(?[\s\S]*)[/])?(?[\s\S]+)$/)?.groups ?? {}
+ const {repository} = await graphql(queries.repositories.repository({owner, name}))
+ repositories.list.push(repository)
+
+ //Format date
+ const time = (Date.now() - new Date(repository.createdAt).getTime()) / (24 * 60 * 60 * 1000)
+ let created = new Date(repository.createdAt).toDateString().substring(4)
+ if (time < 1)
+ created = `${Math.ceil(time * 24)} hour${Math.ceil(time * 24) >= 2 ? "s" : ""} ago`
+ else if (time < 30)
+ created = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
+ repository.created = created
+ }
+
+ //Results
+ return repositories
+ }
+ //Handle errors
+ catch (error) {
+ throw {error:{message:"An error occured", instance:error}}
+ }
+}
\ No newline at end of file
diff --git a/source/plugins/repositories/metadata.yml b/source/plugins/repositories/metadata.yml
new file mode 100644
index 00000000..d18bca69
--- /dev/null
+++ b/source/plugins/repositories/metadata.yml
@@ -0,0 +1,22 @@
+name: "đ Repositories"
+cost: 1 GraphQL request per repository
+category: github
+supports:
+ - user
+ - organization
+inputs:
+
+ # Enable or disable plugin
+ plugin_repositories:
+ description: Display chosen featured repositories
+ type: boolean
+ default: no
+
+ # Featured repositories to display
+ # If no owner is specified, it will implicitly use the current account login as owner
+ plugin_repositories_featured:
+ description: List of repositories to display
+ type: array
+ format: comma-separated
+ default: ""
+ example: lowlighter/metrics
diff --git a/source/plugins/repositories/queries/repository.graphql b/source/plugins/repositories/queries/repository.graphql
new file mode 100644
index 00000000..b4bcb788
--- /dev/null
+++ b/source/plugins/repositories/queries/repository.graphql
@@ -0,0 +1,26 @@
+query RepositoriesRepository {
+ repository(owner: "$owner", name: "$name") {
+ createdAt
+ description
+ forkCount
+ isFork
+ issues {
+ totalCount
+ }
+ nameWithOwner
+ openGraphImageUrl
+ licenseInfo {
+ nickname
+ spdxId
+ name
+ }
+ pullRequests {
+ totalCount
+ }
+ stargazerCount
+ primaryLanguage {
+ color
+ name
+ }
+ }
+}
diff --git a/source/plugins/repositories/tests.yml b/source/plugins/repositories/tests.yml
new file mode 100644
index 00000000..77f25749
--- /dev/null
+++ b/source/plugins/repositories/tests.yml
@@ -0,0 +1,6 @@
+- name: Repositories plugin (default)
+ uses: lowlighter/metrics@latest
+ with:
+ token: MOCKED_TOKEN
+ plugin_repositories: yes
+ plugin_repositories_list: metrics
\ No newline at end of file
diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json
index cc4d8fc5..f88ec37e 100644
--- a/source/templates/classic/partials/_.json
+++ b/source/templates/classic/partials/_.json
@@ -7,6 +7,7 @@
"languages",
"notable",
"projects",
+ "repositories",
"gists",
"pagespeed",
"habits",
@@ -25,6 +26,7 @@
"anilist",
"wakatime",
"skyline",
+ "discussions",
"support",
"stackoverflow",
"stock",
diff --git a/source/templates/classic/partials/discussions.ejs b/source/templates/classic/partials/discussions.ejs
new file mode 100644
index 00000000..e8587b2f
--- /dev/null
+++ b/source/templates/classic/partials/discussions.ejs
@@ -0,0 +1,48 @@
+<% if (plugins.discussions) { %>
+
+
+
+ <% if (!plugins.discussions.error) { %>
+ <%= plugins.discussions.started %> Discussion<%= s(plugins.discussions.started) %> started
+ <% } else { %>
+ Discussions
+ <% } %>
+
+ <% if (plugins.discussions.error) { %>
+
+
+
+
+ <%= plugins.discussions.error.message %>
+
+
+
+ <% } else { %>
+
+
+
+
+ <%= plugins.discussions.comments %> Comment<%= s(plugins.discussions.comments) %>
+
+
+
+
+
+ <%= plugins.discussions.answers %> Answer<%= s(plugins.discussions.answers) %>
+
+
+
+ <% if (Object.keys(plugins.discussions.categories.stats).length) { %>
+
+
+
+ <% for (const [category, posts] of Object.entries(plugins.discussions.categories.stats)) { %>
+
<%= category %>
<%= posts %>
+ <% } %>
+
+
+
+ <% } %>
+ <% } %>
+
+<% } %>
\ No newline at end of file
diff --git a/source/templates/classic/partials/repositories.ejs b/source/templates/classic/partials/repositories.ejs
new file mode 100644
index 00000000..879e2aa2
--- /dev/null
+++ b/source/templates/classic/partials/repositories.ejs
@@ -0,0 +1,78 @@
+<% if (plugins.repositories) { %>
+
+
+
+ Featured repositories
+
+
+
+ <% if (plugins.repositories.error) { %>
+
+
+ <%= plugins.repositories.error.message %>
+
+ <% } else if (plugins.repositories.list.length) { %>
+ <% for (const repository of plugins.repositories.list) { %>
+
+
+
+ <% if (repository.isFork) { %>
+
+ <% } else { %>
+
+ <% } %>
+
+ <%= repository.nameWithOwner %>
+ created <%= repository.created %>
+
+
+
+ <%= repository.description %>
+
+
+ <% if (repository.primaryLanguage) { %>
+
+
+ <%= repository.primaryLanguage.name %>
+
+ <% } %>
+ <% if (repository.licenseInfo) { %>
+
+
+ <%= f.license(repository.licenseInfo) %>
+
+ <% } %>
+
+
+ <%= f(repository.stargazerCount) %>
+
+
+
+ <%= f(repository.forkCount) %>
+
+
+
+ <%= f(repository.issues.totalCount) %>
+
+
+
+ <%= f(repository.pullRequests.totalCount) %>
+
+
+
+
+ <% } %>
+ <% } else { %>
+
+
+
+
+ Configure this plugin with repositories you want to feature!
+
+
+
+ <% } %>
+
+
+
+<% } %>
\ No newline at end of file