diff --git a/source/plugins/calendar/README.md b/source/plugins/calendar/README.md new file mode 100644 index 00000000..3eff2a87 --- /dev/null +++ b/source/plugins/calendar/README.md @@ -0,0 +1,12 @@ + + + +## ➡️ Available options + + + + +## ℹ️ Examples workflows + + + diff --git a/source/plugins/calendar/examples.yml b/source/plugins/calendar/examples.yml new file mode 100644 index 00000000..a986ee3a --- /dev/null +++ b/source/plugins/calendar/examples.yml @@ -0,0 +1,16 @@ +- name: Current year calendar + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.calendar.svg + token: ${{ secrets.METRICS_TOKEN }} + base: "" + plugin_calendar: yes + +- name: Full history calendar + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.calendar.full.svg + token: ${{ secrets.METRICS_TOKEN }} + base: "" + plugin_calendar: yes + plugin_calendar_limit: 0 diff --git a/source/plugins/calendar/index.mjs b/source/plugins/calendar/index.mjs new file mode 100644 index 00000000..9a4f4ecd --- /dev/null +++ b/source/plugins/calendar/index.mjs @@ -0,0 +1,56 @@ +//Setup +export default async function({login, q, data, imports, graphql, queries, account}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.calendar)) + return null + + //Load inputs + let {limit} = imports.metadata.plugins.calendar.inputs({data, account, q}) + + //Compute boundaries + const end = new Date().getFullYear() + const start = new Date(limit ? end-limit+1 : data.user.createdAt, 0).getFullYear() + + //Load contribution calendar + console.debug(`metrics/compute/${login}/plugins > calendar > processing years ${start} to ${end}`) + const calendar = {years:[]} + for (let year = start; year <= end; year++) { + console.debug(`metrics/compute/${login}/plugins > calendar > processing year ${year}`) + const weeks = [] + const newyear = new Date(year, 0, 1) + const endyear = (year === end) ? new Date() : new Date(year, 11, 31) + for (let from = new Date(newyear); from < endyear;) { + //Set date range and ensure we start on sundays + let to = new Date(from) + to.setUTCHours(+4 * 7 * 24) + if (to.getUTCDay()) + to.setUTCHours(-to.getUTCDay() * 24) + if (to > endyear) + to = endyear + + //Ensure that date ranges are not overlapping by setting it to previous day at 23:59:59.999 + const dto = new Date(to) + dto.setUTCHours(-1) + dto.setUTCMinutes(59) + dto.setUTCSeconds(59) + dto.setUTCMilliseconds(999) + //Fetch data from api + console.debug(`metrics/compute/${login}/plugins > calendar > loading calendar from "${from.toISOString()}" to "${dto.toISOString()}"`) + const {user:{calendar:{contributionCalendar}}} = await graphql(queries.isocalendar.calendar({login, from:from.toISOString(), to:dto.toISOString()})) + weeks.push(...contributionCalendar.weeks) + //Set next date range start + from = new Date(to) + } + calendar.years.unshift({year, weeks}) + } + + //Results + return calendar + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } +} \ No newline at end of file diff --git a/source/plugins/calendar/metadata.yml b/source/plugins/calendar/metadata.yml new file mode 100644 index 00000000..b35fc0c8 --- /dev/null +++ b/source/plugins/calendar/metadata.yml @@ -0,0 +1,22 @@ +name: "📆 Calendar" +category: github +description: This plugin displays your commit calendar across several years +examples: + +current year: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.calendar.svg + full history: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.calendar.full.svg +supports: + - user +scopes: + - public_access +inputs: + + plugin_calendar: + description: Enable calendar plugin + type: boolean + default: no + + plugin_calendar_limit: + description: Years to display + type: number + default: 1 + zero: disable \ No newline at end of file diff --git a/source/plugins/calendar/queries/calendar.graphql b/source/plugins/calendar/queries/calendar.graphql new file mode 100644 index 00000000..fb4bc3bd --- /dev/null +++ b/source/plugins/calendar/queries/calendar.graphql @@ -0,0 +1,15 @@ +query CalendarDefault { + user(login: "$login") { + calendar:contributionsCollection(from: "$from", to: "$to") { + contributionCalendar { + weeks { + contributionDays { + contributionCount + color + date + } + } + } + } + } +} \ No newline at end of file diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index d24bb805..f207da59 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -19,6 +19,7 @@ "rss", "tweets", "isocalendar", + "calendar", "stars", "starlists", "stargazers", diff --git a/source/templates/classic/partials/calendar.ejs b/source/templates/classic/partials/calendar.ejs new file mode 100644 index 00000000..a0a77cc8 --- /dev/null +++ b/source/templates/classic/partials/calendar.ejs @@ -0,0 +1,33 @@ +<% if (plugins.calendar) { %> + + + + Contributions calendar + + + + <% if (plugins.calendar.error) { %> + + + <%= plugins.calendar.error.message %> + + <% } else { %> + + <% for (const [r, {year, weeks}] of Object.entries(plugins.calendar.years)) { %> + + <%= year %> + <% for (const [x, week] of Object.entries(weeks)) { %> + + <% for (const [y, {color}] of Object.entries(week.contributionDays)) { %> + + <% } %> + + <% } %> + + <% } %> + + <% } %> + + + +<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 8aa8b0d8..83e180d3 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -838,6 +838,17 @@ max-width: 900px; } +/* Calendar */ + svg.calendar { + margin-left: 13px; + margin-top: 4px; + } + + svg.calendar text { + font-size: 18px; + fill: currentColor; + } + /* People */ .people { padding: 0 10px; diff --git a/tests/mocks/api/github/graphql/calendar.default.mjs b/tests/mocks/api/github/graphql/calendar.default.mjs new file mode 100644 index 00000000..716db281 --- /dev/null +++ b/tests/mocks/api/github/graphql/calendar.default.mjs @@ -0,0 +1,32 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > calendar/default") + //Generate calendar + const date = new Date(query.match(/from: "(?\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: "(?\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, faker.datatype.number(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, + }, + }, + }, + }) +}