From c5a5fecd3689b11fd3325c7f0fd3228bd368e0f5 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Wed, 26 Apr 2023 13:33:58 +0200 Subject: [PATCH 01/12] feat: adding utilities for cards local generation & testing (#34) * feat: getuserdata private * fix: fix incorrect import * feat: refractor & add local scripts * feat: add local dev generation script * last touches * refractor: suggestions * docs: add documentation for local dev script * refractor: change output folder * update local dev user command name --- README.md | 12 ++++ package.json | 1 + src/social-card/social-card.service.ts | 88 +++++++++++++++----------- test/app.e2e-spec.ts | 2 +- test/local-dev/UserCards.ts | 36 +++++++++++ 5 files changed, 100 insertions(+), 39 deletions(-) create mode 100644 test/local-dev/UserCards.ts diff --git a/README.md b/README.md index f9b0e6a..8ece0ef 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,18 @@ To start a local copy of the app on port `3001`: npm run start:dev ``` +### Local dev scripts + +There are a few scripts that can be used to generate and test the social cards locally without having to deploy to the CDN. This is the way to go when developing & testing the interface for the social cards. + +#### Generating user profile cards: + +```shell +npm run local-dev:usercards +``` + +> Generates user cards for all users in the test array inside `test/local-dev/UserCards.ts` and outputs them in `dist/local-dev/` for testing. + ### 📝 Environment variables Some environment variables are required to run the application. You can find them in the `.env.example` file. While most of them are optional, some are required to run the application. diff --git a/package.json b/package.json index d91ee86..b1caf4c 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test:cov": "npm run test --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "npm run test --config test/jest-e2e.json", + "test:local:user": "npx ts-node test/local-dev/UserCards", "docs": "npx compodoc -p tsconfig.json --hideGenerator --disableDependencies -d ./dist/documentation ./src", "docs:serve": "npm run docs -- --serve" }, diff --git a/src/social-card/social-card.service.ts b/src/social-card/social-card.service.ts index 9fcace5..b0a0006 100644 --- a/src/social-card/social-card.service.ts +++ b/src/social-card/social-card.service.ts @@ -2,7 +2,8 @@ import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nest import { HttpService } from "@nestjs/axios"; import { Resvg } from "@resvg/resvg-js"; import { Repository, Language, User } from "@octokit/graphql-schema"; -import { readFile } from "node:fs/promises"; +import fs from "node:fs/promises"; + import { GithubService } from "../github/github.service"; import { S3FileStorageService } from "../s3-file-storage/s3-file-storage.service"; @@ -18,6 +19,18 @@ interface RequiresUpdateMeta { lastModified: Date | null, } +interface UserCardData { + id: User["databaseId"], + name: User["name"], + langs: (Language & { + size: number, + })[], + langTotal: number, + repos: Repository[], + avatarUrl: string, +} + + @Injectable() export class SocialCardService { private readonly logger = new Logger(this.constructor.name); @@ -28,16 +41,7 @@ export class SocialCardService { private readonly s3FileStorageService: S3FileStorageService, ) {} - async getUserData (username: string): Promise<{ - id: User["databaseId"], - name: User["name"], - langs: (Language & { - size: number, - })[], - langTotal: number, - repos: Repository[], - avatarUrl: string, - }> { + private async getUserData (username: string): Promise { const langs: Record = {}; @@ -74,6 +78,38 @@ export class SocialCardService { }; } + // public only to be used in local scripts. Not for controller direct use. + async generateCardBuffer (username: string, userData?: UserCardData) { + const { html } = await import("satori-html"); + const satori = (await import("satori")).default; + + const { avatarUrl, repos, langs, langTotal } = userData ? userData : await this.getUserData(username); + + const template = html(userProfileCard(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos))); + + const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); + + const svg = await satori(template, { + width: 1200, + height: 627, + fonts: [ + { + name: "Inter", + data: interArrayBuffer, + weight: 400, + style: "normal", + }, + ], + tailwindConfig, + }); + + const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" }); + + const pngData = resvg.render(); + + return { png: pngData.asPng(), svg }; + } + async checkRequiresUpdate (username: string): Promise { const hash = `users/${String(username)}.png`; const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`; @@ -107,39 +143,15 @@ export class SocialCardService { throw new ForbiddenException("Rate limit exceeded"); } - const { html } = await import("satori-html"); - const satori = (await import("satori")).default; + const userData = await this.getUserData(username); try { - const { id, avatarUrl, repos, langs, langTotal } = await this.getUserData(username); const hash = `users/${String(username)}.png`; const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`; - const template = html(userProfileCard(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos))); - - const interArrayBuffer = await readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); - - const svg = await satori(template, { - width: 1200, - height: 627, - fonts: [ - { - name: "Inter", - data: interArrayBuffer, - weight: 400, - style: "normal", - }, - ], - tailwindConfig, - }); - - const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" }); - - const pngData = resvg.render(); - - const pngBuffer = pngData.asPng(); + const { png } = await this.generateCardBuffer(username, userData); - await this.s3FileStorageService.uploadFile(pngBuffer, hash, "image/png", { "x-amz-meta-user-id": String(id) }); + await this.s3FileStorageService.uploadFile(png, hash, "image/png", { "x-amz-meta-user-id": String(userData.id) }); this.logger.debug(`User ${username} did not exist in S3, generated image and uploaded to S3, redirecting`); diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 50cda62..9ceebc4 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import request from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { diff --git a/test/local-dev/UserCards.ts b/test/local-dev/UserCards.ts new file mode 100644 index 0000000..d0c0c9c --- /dev/null +++ b/test/local-dev/UserCards.ts @@ -0,0 +1,36 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AppModule } from "../../src/app.module"; +import { SocialCardService } from "../../src/social-card/social-card.service"; +import { existsSync } from "node:fs"; +import { mkdir, writeFile } from "fs/promises"; + + +const testUsernames = [ + "bdougie", "deadreyo", "defunkt", "0-vortex", +]; + +const folderPath = "dist"; + +async function testUserCards () { + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); + + const app = moduleFixture.createNestApplication(); + + await app.init(); + + const instance = app.get(SocialCardService); + + const promises = testUsernames.map(async username => { + const { svg } = await instance.generateCardBuffer(username); + + if (!existsSync(folderPath)) { + await mkdir(folderPath); + } + await writeFile(`${folderPath}/${username}.svg`, svg); + }); + + // generating sequential: 10.5 seconds, parallel: 4.5 seconds + await Promise.all(promises); +} + +testUserCards(); From ec3e554ea6bc3c0fa12aab6496bbfafdd01edf90 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Wed, 26 Apr 2023 11:37:30 +0000 Subject: [PATCH 02/12] chore(minor): release 2.1.0-beta.1 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.1](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.0.1...v2.1.0-beta.1) (2023-04-26) ### 🍕 Features * adding utilities for cards local generation & testing ([#34](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/34)) ([c5a5fec](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/c5a5fecd3689b11fd3325c7f0fd3228bd368e0f5)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8100cff..0d059cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.1](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.0.1...v2.1.0-beta.1) (2023-04-26) + + +### 🍕 Features + +* adding utilities for cards local generation & testing ([#34](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/34)) ([c5a5fec](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/c5a5fecd3689b11fd3325c7f0fd3228bd368e0f5)) + ### [2.0.1](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.0.0...v2.0.1) (2023-04-19) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ee5a535..757c1b6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.0.1", + "version": "2.1.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.0.1", + "version": "2.1.0-beta.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b1caf4c..65b57d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.0.1", + "version": "2.1.0-beta.1", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index de51b55..e557d52 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfigtemplatestemplatessocial-card.service.tssocial-card.service.tssocial-card.service.tssocial-card.controll...social-card.controll...social-card.controll...social-car...social-car...social-car...s3-file-storage.serv...s3-file-storage.serv...s3-file-storage.serv...health.controller.tshealth.controller.tshealth.controller.tsgql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.service.tsgithub.service.tsgithub.service.tsapi.config.tsapi.config.tsapi.config.tsdigital-oce...digital-oce...digital-oce...github.c...github.c...github.c...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tstailwind.config.tstailwind.config.tstailwind.config.tsuser-profile-c...user-profile-c...user-profile-c...user-langs.tsuser-langs.tsuser-langs.tsuser-profil...user-profil...user-profil...repo-icon-...repo-icon-...repo-icon-....tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfigtemplatestemplatessocial-card.service.tssocial-card.service.tssocial-card.service.tssocial-card.controll...social-card.controll...social-card.controll...social-car...social-car...social-car...s3-file-storage.serv...s3-file-storage.serv...s3-file-storage.serv...health.controller.tshealth.controller.tshealth.controller.tsgql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.service.tsgithub.service.tsgithub.service.tsapi.config.tsapi.config.tsapi.config.tsdigital-oce...digital-oce...digital-oce...github.c...github.c...github.c...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tstailwind.config.tstailwind.config.tstailwind.config.tsuser-profile-...user-profile-...user-profile-...user-langs.tsuser-langs.tsuser-langs.tsuser-profil...user-profil...user-profil...repo-icon-...repo-icon-...repo-icon-....tseach dot sized by file size \ No newline at end of file From 3dc1a0add116c1124af6a993e9e843015f3c0d25 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Sun, 30 Apr 2023 18:28:23 +0200 Subject: [PATCH 03/12] fix: Overflowing language bar fix (#38) * test: add more cards * fix: fix overflowing language bar --- src/social-card/templates/user-langs.ts | 4 ++-- src/social-card/templates/user-profile-card.ts | 2 +- test/local-dev/UserCards.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/social-card/templates/user-langs.ts b/src/social-card/templates/user-langs.ts index 09bebda..17d8e77 100644 --- a/src/social-card/templates/user-langs.ts +++ b/src/social-card/templates/user-langs.ts @@ -2,11 +2,11 @@ import { Language } from "@octokit/graphql-schema"; const userLangs = (langs: (Language & { size: number })[], totalCount = 0, joinLiteral = "") => { const filteredLangs = langs - .filter(({ size }) => size > 0 && !((size / totalCount) * 100 < 0.5)); + .filter(({ size }) => size > 0 && (size / totalCount) * 100 >= 0.5); return filteredLangs .map(({ color, size }) => { - const realPercent = Math.round(size / totalCount * 100); + const realPercent = size / totalCount * 100; return `
`; }) diff --git a/src/social-card/templates/user-profile-card.ts b/src/social-card/templates/user-profile-card.ts index 910ab2c..3e2bc52 100644 --- a/src/social-card/templates/user-profile-card.ts +++ b/src/social-card/templates/user-profile-card.ts @@ -21,7 +21,7 @@ const userProfileCard = (avatarUrl: string, name: string, langs: string, repos: ${repos}
-
+
${langs}
diff --git a/test/local-dev/UserCards.ts b/test/local-dev/UserCards.ts index d0c0c9c..c687e7b 100644 --- a/test/local-dev/UserCards.ts +++ b/test/local-dev/UserCards.ts @@ -6,7 +6,7 @@ import { mkdir, writeFile } from "fs/promises"; const testUsernames = [ - "bdougie", "deadreyo", "defunkt", "0-vortex", + "bdougie", "deadreyo", "defunkt", "0-vortex", "Anush008", "diivi" ]; const folderPath = "dist"; From 58c00b5e02b8b771fb95a11bb2bbaf1a4919179d Mon Sep 17 00:00:00 2001 From: NightKnight Date: Sun, 30 Apr 2023 16:31:58 +0000 Subject: [PATCH 04/12] chore(patch): release 2.1.0-beta.2 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.2](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.1...v2.1.0-beta.2) (2023-04-30) ### 🐛 Bug Fixes * Overflowing language bar fix ([#38](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/38)) ([3dc1a0a](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/3dc1a0add116c1124af6a993e9e843015f3c0d25)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d059cf..56f8e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.2](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.1...v2.1.0-beta.2) (2023-04-30) + + +### 🐛 Bug Fixes + +* Overflowing language bar fix ([#38](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/38)) ([3dc1a0a](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/3dc1a0add116c1124af6a993e9e843015f3c0d25)) + ## [2.1.0-beta.1](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.0.1...v2.1.0-beta.1) (2023-04-26) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 757c1b6..1637388 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.1", + "version": "2.1.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.1", + "version": "2.1.0-beta.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 65b57d6..828840d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.1", + "version": "2.1.0-beta.2", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index e557d52..2d3d02f 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfigtemplatestemplatessocial-card.service.tssocial-card.service.tssocial-card.service.tssocial-card.controll...social-card.controll...social-card.controll...social-car...social-car...social-car...s3-file-storage.serv...s3-file-storage.serv...s3-file-storage.serv...health.controller.tshealth.controller.tshealth.controller.tsgql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.service.tsgithub.service.tsgithub.service.tsapi.config.tsapi.config.tsapi.config.tsdigital-oce...digital-oce...digital-oce...github.c...github.c...github.c...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tstailwind.config.tstailwind.config.tstailwind.config.tsuser-profile-...user-profile-...user-profile-...user-langs.tsuser-langs.tsuser-langs.tsuser-profil...user-profil...user-profil...repo-icon-...repo-icon-...repo-icon-....tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfigtemplatestemplatessocial-card.service.tssocial-card.service.tssocial-card.service.tssocial-card.controll...social-card.controll...social-card.controll...social-car...social-car...social-car...s3-file-storage.serv...s3-file-storage.serv...s3-file-storage.serv...health.controller.tshealth.controller.tshealth.controller.tsgql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.service.tsgithub.service.tsgithub.service.tsapi.config.tsapi.config.tsapi.config.tsdigital-oce...digital-oce...digital-oce...github.c...github.c...github.c...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tstailwind.config.tstailwind.config.tstailwind.config.tsuser-profile-c...user-profile-c...user-profile-c...user-profil...user-profil...user-profil...user-langs.tsuser-langs.tsuser-langs.tsrepo-icon-...repo-icon-...repo-icon-....tseach dot sized by file size \ No newline at end of file From 138a847bad92eb1f35fafa467720de01646f31cc Mon Sep 17 00:00:00 2001 From: NightKnight Date: Tue, 2 May 2023 09:14:13 +0300 Subject: [PATCH 05/12] feat: Highlight Cards UI Generation (frontend-only) (#36) * feat: get highlight data function * feat: setup highlight testing * feat: data interfaces * refractor: split card codes * feat: highlight cards code structure * refractor: template shared folder * refractor: card footer component * refractor: separate o ut card style setup * feat: highlights UI complete * lint --- package.json | 1 + src/app.module.ts | 6 +- .../highlight-card/highlight-card.module.ts | 11 ++ .../highlight-card/highlight-card.service.ts | 113 ++++++++++++++++++ src/social-card/social-card.module.ts | 14 --- .../templates/highlight-card.template.ts | 30 +++++ .../templates/shared/card-footer.ts | 31 +++++ .../templates/shared/card-style-setup.ts | 10 ++ .../{ => shared}/repo-icon-with-name.ts | 0 .../templates/{ => shared}/user-langs.ts | 0 .../user-repos.ts} | 6 +- ...-card.ts => user-profile-card.template.ts} | 25 ++-- .../user-card.controller.ts} | 12 +- src/social-card/user-card/user-card.module.ts | 14 +++ .../user-card.service.ts} | 16 +-- test/local-dev/HighlightCards.ts | 33 +++++ test/local-dev/UserCards.ts | 4 +- tsconfig.json | 3 + typings/global.d.ts | 17 +++ 19 files changed, 293 insertions(+), 53 deletions(-) create mode 100644 src/social-card/highlight-card/highlight-card.module.ts create mode 100644 src/social-card/highlight-card/highlight-card.service.ts delete mode 100644 src/social-card/social-card.module.ts create mode 100644 src/social-card/templates/highlight-card.template.ts create mode 100644 src/social-card/templates/shared/card-footer.ts create mode 100644 src/social-card/templates/shared/card-style-setup.ts rename src/social-card/templates/{ => shared}/repo-icon-with-name.ts (100%) rename src/social-card/templates/{ => shared}/user-langs.ts (100%) rename src/social-card/templates/{user-profile-repos.ts => shared/user-repos.ts} (69%) rename src/social-card/templates/{user-profile-card.ts => user-profile-card.template.ts} (54%) rename src/social-card/{social-card.controller.ts => user-card/user-card.controller.ts} (84%) create mode 100644 src/social-card/user-card/user-card.module.ts rename src/social-card/{social-card.service.ts => user-card/user-card.service.ts} (89%) create mode 100644 test/local-dev/HighlightCards.ts create mode 100644 typings/global.d.ts diff --git a/package.json b/package.json index 828840d..e21e5f1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "npm run test --config test/jest-e2e.json", "test:local:user": "npx ts-node test/local-dev/UserCards", + "test:local:highlight": "npx ts-node test/local-dev/HighlightCards", "docs": "npx compodoc -p tsconfig.json --hideGenerator --disableDependencies -d ./dist/documentation ./src", "docs:serve": "npm run docs -- --serve" }, diff --git a/src/app.module.ts b/src/app.module.ts index 6b2121c..e363c03 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,8 +8,9 @@ import { clc } from "@nestjs/common/utils/cli-colors.util"; import ApiConfig from "./config/api.config"; import GitHubConfig from "./config/github.config"; import DigitalOceanConfig from "./config/digital-ocean.config"; -import { SocialCardModule } from "./social-card/social-card.module"; +import { UserCardModule } from "./social-card/user-card/user-card.module"; import { S3FileStorageModule } from "./s3-file-storage/s3-file-storage.module"; +import { HighlightCardModule } from "./social-card/highlight-card/highlight-card.module"; @Module({ imports: [ @@ -46,8 +47,9 @@ import { S3FileStorageModule } from "./s3-file-storage/s3-file-storage.module"; }), TerminusModule, HttpModule, - SocialCardModule, S3FileStorageModule, + UserCardModule, + HighlightCardModule, ], controllers: [], providers: [], diff --git a/src/social-card/highlight-card/highlight-card.module.ts b/src/social-card/highlight-card/highlight-card.module.ts new file mode 100644 index 0000000..6eb0d70 --- /dev/null +++ b/src/social-card/highlight-card/highlight-card.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { HttpModule } from "@nestjs/axios"; +import { GithubModule } from "../../github/github.module"; +import { S3FileStorageModule } from "../../s3-file-storage/s3-file-storage.module"; +import { HighlightCardService } from "../highlight-card/highlight-card.service"; + +@Module({ + imports: [HttpModule, GithubModule, S3FileStorageModule], + providers: [HighlightCardService], +}) +export class HighlightCardModule {} diff --git a/src/social-card/highlight-card/highlight-card.service.ts b/src/social-card/highlight-card/highlight-card.service.ts new file mode 100644 index 0000000..3c9c586 --- /dev/null +++ b/src/social-card/highlight-card/highlight-card.service.ts @@ -0,0 +1,113 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { HttpService } from "@nestjs/axios"; +import { Resvg } from "@resvg/resvg-js"; +import { Repository, Language } from "@octokit/graphql-schema"; +import fs from "node:fs/promises"; + +import { GithubService } from "../../github/github.service"; +import { S3FileStorageService } from "../../s3-file-storage/s3-file-storage.service"; +import userLangs from "../templates/shared/user-langs"; +import userProfileRepos from "../templates/shared/user-repos"; +import tailwindConfig from "../templates/tailwind.config"; +import { firstValueFrom } from "rxjs"; +import highlightCardTemplate from "../templates/highlight-card.template"; + +interface HighlightCardData { + title: string, + body: string, + reactions: number, + avatarUrl: string, + repos: Repository[], + langTotal: number, + langs: (Language & { + size: number, + })[], +} + +@Injectable() +export class HighlightCardService { + private readonly logger = new Logger(this.constructor.name); + + constructor ( + private readonly httpService: HttpService, + private readonly githubService: GithubService, + private readonly s3FileStorageService: S3FileStorageService, + ) {} + + private async getHighlightData (highlightId: number): Promise { + const langs: Record = {}; + const today = (new Date); + const today30daysAgo = new Date((new Date).setDate(today.getDate() - 30)); + + const highlightReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/user/highlights/${highlightId}`)); + const { login, title, highlight: body } = highlightReq.data; + + const reactionsReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/highlights/${highlightId}/reactions`)); + const reactions = reactionsReq.data.reduce( (acc, curr) => acc + Number(curr.reaction_count), 0); + + const user = await this.githubService.getUser(login); + const langRepos = user.repositories.nodes?.filter(repo => new Date(String(repo?.pushedAt)) > today30daysAgo) as Repository[]; + let langTotal = 0; + + langRepos.forEach(repo => { + repo.languages?.edges?.forEach(edge => { + if (edge?.node.id) { + langTotal += edge.size; + + if (!Object.keys(langs).includes(edge.node.id)) { + langs[edge.node.id] = { + ...edge.node, + size: edge.size, + }; + } else { + langs[edge.node.id].size += edge.size; + } + } + }); + }); + + return { + title, + body, + reactions, + avatarUrl: `${String(user.avatarUrl)}&size=150`, + langs: Array.from(Object.values(langs)).sort((a, b) => b.size - a.size), + langTotal, + repos: user.topRepositories.nodes?.filter(repo => !repo?.isPrivate && repo?.owner.login !== login) as Repository[], + }; + } + + // public only to be used in local scripts. Not for controller direct use. + async generateCardBuffer (highlightId: number, highlightData?: HighlightCardData) { + const { html } = await import("satori-html"); + const satori = (await import("satori")).default; + + const { title, body, reactions, avatarUrl, repos, langs, langTotal } = highlightData ? highlightData : await this.getHighlightData(highlightId); + + const template = html(highlightCardTemplate(avatarUrl, title, body, userLangs(langs, langTotal), userProfileRepos(repos, 2), reactions)); + + const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); + + const svg = await satori(template, { + width: 1200, + height: 627, + fonts: [ + { + name: "Inter", + data: interArrayBuffer, + weight: 400, + style: "normal", + }, + ], + tailwindConfig, + }); + + const resvg = new Resvg(svg, { background: "rgba(238, 235, 230, .9)" }); + + const pngData = resvg.render(); + + return { png: pngData.asPng(), svg }; + } +} diff --git a/src/social-card/social-card.module.ts b/src/social-card/social-card.module.ts deleted file mode 100644 index bef529e..0000000 --- a/src/social-card/social-card.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from "@nestjs/common"; -import { HttpModule } from "@nestjs/axios"; - -import { SocialCardService } from "./social-card.service"; -import { SocialCardController } from "./social-card.controller"; -import { GithubModule } from "../github/github.module"; -import { S3FileStorageModule } from "../s3-file-storage/s3-file-storage.module"; - -@Module({ - imports: [HttpModule, GithubModule, S3FileStorageModule], - providers: [SocialCardService], - controllers: [SocialCardController], -}) -export class SocialCardModule {} diff --git a/src/social-card/templates/highlight-card.template.ts b/src/social-card/templates/highlight-card.template.ts new file mode 100644 index 0000000..15a2eda --- /dev/null +++ b/src/social-card/templates/highlight-card.template.ts @@ -0,0 +1,30 @@ +import cardFooter from "./shared/card-footer"; +import cardStyleSetup from "./shared/card-style-setup"; + +const highlightCardTemplate = (avatarUrl: string, title: string, body: string, langs: string, repos: string, reactions: number): string => ` + ${cardStyleSetup} + +
+
+
+ +
+ +
+

+ ${title} +

+

+ ${body.length > 108 ? `${body.slice(0, 108)}...` : body} +

+
+ +
+ +
+
+ + ${cardFooter(langs, repos, reactions)} +
`; + +export default highlightCardTemplate; diff --git a/src/social-card/templates/shared/card-footer.ts b/src/social-card/templates/shared/card-footer.ts new file mode 100644 index 0000000..6ea08ec --- /dev/null +++ b/src/social-card/templates/shared/card-footer.ts @@ -0,0 +1,31 @@ + +const heartIconData = `data:image/svg+xml,%3csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill-rule='evenodd' clip-rule='evenodd' d='M6.73649 2.5C3.82903 2.5 1 5.052 1 8.51351C1 12.3318 3.80141 15.5735 6.38882 17.7763C7.70549 18.8973 9.01844 19.7929 10.0004 20.4077C10.4922 20.7157 10.9029 20.9544 11.1922 21.1169C11.4093 21.2388 11.5582 21.318 11.6223 21.3516C11.7407 21.4132 11.8652 21.4527 12 21.4527C12.1193 21.4527 12.2378 21.4238 12.3438 21.3693C12.5003 21.2886 12.6543 21.2031 12.8078 21.1169C13.0971 20.9544 13.5078 20.7157 13.9996 20.4077C14.9816 19.7929 16.2945 18.8973 17.6112 17.7763C20.1986 15.5735 23 12.3318 23 8.51351C23 5.052 20.171 2.5 17.2635 2.5C14.9702 2.5 13.1192 3.72621 12 5.60482C10.8808 3.72621 9.02981 2.5 6.73649 2.5ZM6.73649 4C4.65746 4 2.5 5.88043 2.5 8.51351C2.5 11.6209 4.8236 14.4738 7.36118 16.6342C8.60701 17.6948 9.85656 18.5479 10.7965 19.1364C11.2656 19.4301 11.6557 19.6567 11.9269 19.8091L12 19.85L12.0731 19.8091C12.3443 19.6567 12.7344 19.4301 13.2035 19.1364C14.1434 18.5479 15.393 17.6948 16.6388 16.6342C19.1764 14.4738 21.5 11.6209 21.5 8.51351C21.5 5.88043 19.3425 4 17.2635 4C15.1581 4 13.4627 5.38899 12.7115 7.64258C12.6094 7.94883 12.3228 8.15541 12 8.15541C11.6772 8.15541 11.3906 7.94883 11.2885 7.64258C10.5373 5.38899 8.84185 4 6.73649 4Z' fill='%2324292F'/%3e%3c/svg%3e`; + +const cardFooter = (langs: string, repos: string, reactions?: number) => ` +
+
+
+ ${repos} +
+ + ${reactions + ? ` +
+ + + ${reactions} Reactions + +
+ ` + : ""} +
+ +
+
+ ${langs} +
+
+
+ `; + +export default cardFooter; diff --git a/src/social-card/templates/shared/card-style-setup.ts b/src/social-card/templates/shared/card-style-setup.ts new file mode 100644 index 0000000..f3b633d --- /dev/null +++ b/src/social-card/templates/shared/card-style-setup.ts @@ -0,0 +1,10 @@ + +const cardStyleSetup = ` + +`; + +export default cardStyleSetup; diff --git a/src/social-card/templates/repo-icon-with-name.ts b/src/social-card/templates/shared/repo-icon-with-name.ts similarity index 100% rename from src/social-card/templates/repo-icon-with-name.ts rename to src/social-card/templates/shared/repo-icon-with-name.ts diff --git a/src/social-card/templates/user-langs.ts b/src/social-card/templates/shared/user-langs.ts similarity index 100% rename from src/social-card/templates/user-langs.ts rename to src/social-card/templates/shared/user-langs.ts diff --git a/src/social-card/templates/user-profile-repos.ts b/src/social-card/templates/shared/user-repos.ts similarity index 69% rename from src/social-card/templates/user-profile-repos.ts rename to src/social-card/templates/shared/user-repos.ts index eec1dc7..4ab9ed2 100644 --- a/src/social-card/templates/user-profile-repos.ts +++ b/src/social-card/templates/shared/user-repos.ts @@ -1,12 +1,12 @@ import repoIconWithName from "./repo-icon-with-name"; import { Repository } from "@octokit/graphql-schema"; -const userProfileRepos = (repos: Repository[]): string => { +const userProfileRepos = (repos: Repository[], limit: number): string => { const repoList = repos.map(({ name, owner: { avatarUrl } }) => repoIconWithName(`${name.substring(0, 15).replace(/\.+$/, "")}${name.length > 15 ? "..." : ""}`, `${String(avatarUrl)}&size=40`)); - return `${repoList.slice(0, 4).join("")}${repoList.length > 4 - ? `

+${repoList.length - 2}

` + return `${repoList.slice(0, limit).join("")}${repoList.length > limit + ? `

+${repoList.length - limit}

` : ``}`; }; diff --git a/src/social-card/templates/user-profile-card.ts b/src/social-card/templates/user-profile-card.template.ts similarity index 54% rename from src/social-card/templates/user-profile-card.ts rename to src/social-card/templates/user-profile-card.template.ts index 3e2bc52..69274ac 100644 --- a/src/social-card/templates/user-profile-card.ts +++ b/src/social-card/templates/user-profile-card.template.ts @@ -1,9 +1,8 @@ -const userProfileCard = (avatarUrl: string, name: string, langs: string, repos: string): string => ` - +import cardFooter from "./shared/card-footer"; +import cardStyleSetup from "./shared/card-style-setup"; + +const userProfileCardTemplate = (avatarUrl: string, name: string, langs: string, repos: string): string => ` + ${cardStyleSetup}
@@ -16,17 +15,7 @@ const userProfileCard = (avatarUrl: string, name: string, langs: string, repos:
-
-
- ${repos} -
- -
-
- ${langs} -
-
-
+ ${cardFooter(langs, repos)}
`; -export default userProfileCard; +export default userProfileCardTemplate; diff --git a/src/social-card/social-card.controller.ts b/src/social-card/user-card/user-card.controller.ts similarity index 84% rename from src/social-card/social-card.controller.ts rename to src/social-card/user-card/user-card.controller.ts index 5d8e6d2..4858e5b 100644 --- a/src/social-card/social-card.controller.ts +++ b/src/social-card/user-card/user-card.controller.ts @@ -9,13 +9,13 @@ import { } from "@nestjs/swagger"; import { FastifyReply } from "fastify"; -import { SocialCardService } from "./social-card.service"; +import { UserCardService } from "./user-card.service"; @Controller("users") @ApiTags("User social cards") -export class SocialCardController { +export class UserCardController { constructor ( - private readonly socialCardService: SocialCardService, + private readonly userCardService: UserCardService, ) {} @Get("/:username") @@ -33,13 +33,13 @@ export class SocialCardController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { const sanitizedUsername = username.toLowerCase(); - const { fileUrl, hasFile, needsUpdate } = await this.socialCardService.checkRequiresUpdate(sanitizedUsername); + const { fileUrl, hasFile, needsUpdate } = await this.userCardService.checkRequiresUpdate(sanitizedUsername); if (hasFile && !needsUpdate) { return res.status(HttpStatus.FOUND).redirect(fileUrl); } - const url = await this.socialCardService.getUserCard(sanitizedUsername); + const url = await this.userCardService.getUserCard(sanitizedUsername); return res.status(HttpStatus.FOUND).redirect(url); } @@ -57,7 +57,7 @@ export class SocialCardController { @Res({ passthrough: true }) res: FastifyReply, ): Promise { const sanitizedUsername = username.toLowerCase(); - const { fileUrl, hasFile, needsUpdate, lastModified } = await this.socialCardService.checkRequiresUpdate(sanitizedUsername); + const { fileUrl, hasFile, needsUpdate, lastModified } = await this.userCardService.checkRequiresUpdate(sanitizedUsername); return res .headers({ diff --git a/src/social-card/user-card/user-card.module.ts b/src/social-card/user-card/user-card.module.ts new file mode 100644 index 0000000..5d8e8df --- /dev/null +++ b/src/social-card/user-card/user-card.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; +import { HttpModule } from "@nestjs/axios"; + +import { UserCardService } from "./user-card.service"; +import { UserCardController } from "./user-card.controller"; +import { GithubModule } from "../../github/github.module"; +import { S3FileStorageModule } from "../../s3-file-storage/s3-file-storage.module"; + +@Module({ + imports: [HttpModule, GithubModule, S3FileStorageModule], + providers: [UserCardService], + controllers: [UserCardController], +}) +export class UserCardModule {} diff --git a/src/social-card/social-card.service.ts b/src/social-card/user-card/user-card.service.ts similarity index 89% rename from src/social-card/social-card.service.ts rename to src/social-card/user-card/user-card.service.ts index b0a0006..b64edb3 100644 --- a/src/social-card/social-card.service.ts +++ b/src/social-card/user-card/user-card.service.ts @@ -5,12 +5,12 @@ import { Repository, Language, User } from "@octokit/graphql-schema"; import fs from "node:fs/promises"; -import { GithubService } from "../github/github.service"; -import { S3FileStorageService } from "../s3-file-storage/s3-file-storage.service"; -import userLangs from "./templates/user-langs"; -import userProfileRepos from "./templates/user-profile-repos"; -import userProfileCard from "./templates/user-profile-card"; -import tailwindConfig from "./templates/tailwind.config"; +import { GithubService } from "../../github/github.service"; +import { S3FileStorageService } from "../../s3-file-storage/s3-file-storage.service"; +import userLangs from "../templates/shared/user-langs"; +import userProfileRepos from "../templates/shared/user-repos"; +import userProfileCardTemplate from "../templates/user-profile-card.template"; +import tailwindConfig from "../templates/tailwind.config"; interface RequiresUpdateMeta { fileUrl: string, @@ -32,7 +32,7 @@ interface UserCardData { @Injectable() -export class SocialCardService { +export class UserCardService { private readonly logger = new Logger(this.constructor.name); constructor ( @@ -85,7 +85,7 @@ export class SocialCardService { const { avatarUrl, repos, langs, langTotal } = userData ? userData : await this.getUserData(username); - const template = html(userProfileCard(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos))); + const template = html(userProfileCardTemplate(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos, 4))); const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); diff --git a/test/local-dev/HighlightCards.ts b/test/local-dev/HighlightCards.ts new file mode 100644 index 0000000..64d82a7 --- /dev/null +++ b/test/local-dev/HighlightCards.ts @@ -0,0 +1,33 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { AppModule } from "../../src/app.module"; +import { existsSync } from "node:fs"; +import { mkdir, writeFile } from "fs/promises"; +import { HighlightCardService } from "../../src/social-card/highlight-card/highlight-card.service"; + +const testHighlights = [102, 101, 103]; + +const folderPath = "dist"; + +async function testHighlightCards () { + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule] }).compile(); + + const app = moduleFixture.createNestApplication(); + + await app.init(); + + const instance = app.get(HighlightCardService); + + const promises = testHighlights.map(async id => { + const { svg } = await instance.generateCardBuffer(id); + + if (!existsSync(folderPath)) { + await mkdir(folderPath); + } + await writeFile(`${folderPath}/${id}.svg`, svg); + }); + + // generating sequential: 10.5 seconds, parallel: 4.5 seconds + await Promise.all(promises); +} + +testHighlightCards(); diff --git a/test/local-dev/UserCards.ts b/test/local-dev/UserCards.ts index c687e7b..fe8aae8 100644 --- a/test/local-dev/UserCards.ts +++ b/test/local-dev/UserCards.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from "@nestjs/testing"; import { AppModule } from "../../src/app.module"; -import { SocialCardService } from "../../src/social-card/social-card.service"; +import { UserCardService } from "../../src/social-card/user-card/user-card.service"; import { existsSync } from "node:fs"; import { mkdir, writeFile } from "fs/promises"; @@ -18,7 +18,7 @@ async function testUserCards () { await app.init(); - const instance = app.get(SocialCardService); + const instance = app.get(UserCardService); const promises = testUsernames.map(async username => { const { svg } = await instance.generateCardBuffer(username); diff --git a/tsconfig.json b/tsconfig.json index a7f97d6..c14009f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,7 @@ { + "ts-node": { + "files": true + }, "compilerOptions": { // "rootDir": "./src", "strict": true, diff --git a/typings/global.d.ts b/typings/global.d.ts new file mode 100644 index 0000000..dbd449f --- /dev/null +++ b/typings/global.d.ts @@ -0,0 +1,17 @@ +interface DbHighlight { + readonly id: string; + readonly user_id: string; + readonly url: string; + readonly title: string; + readonly highlight: string; + readonly pinned: boolean; + readonly created_at: string; + readonly updated_at: string; + readonly deleted_at: string | null; + readonly login: string; +} + +interface DbReaction { + emoji_id: string; + reaction_count: string; +} From e61f754fdc6798430a263b18c355a6deb8cf719a Mon Sep 17 00:00:00 2001 From: NightKnight Date: Tue, 2 May 2023 06:17:41 +0000 Subject: [PATCH 06/12] chore(minor): release 2.1.0-beta.3 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.3](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.2...v2.1.0-beta.3) (2023-05-02) ### 🍕 Features * Highlight Cards UI Generation (frontend-only) ([#36](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/36)) ([138a847](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/138a847bad92eb1f35fafa467720de01646f31cc)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f8e42..adf5c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.3](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.2...v2.1.0-beta.3) (2023-05-02) + + +### 🍕 Features + +* Highlight Cards UI Generation (frontend-only) ([#36](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/36)) ([138a847](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/138a847bad92eb1f35fafa467720de01646f31cc)) + ## [2.1.0-beta.2](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.1...v2.1.0-beta.2) (2023-04-30) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1637388..d8db9cb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.2", + "version": "2.1.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.2", + "version": "2.1.0-beta.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e21e5f1..ea3d3fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.2", + "version": "2.1.0-beta.3", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index 2d3d02f..3f9f7cd 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfigtemplatestemplatessocial-card.service.tssocial-card.service.tssocial-card.service.tssocial-card.controll...social-card.controll...social-card.controll...social-car...social-car...social-car...s3-file-storage.serv...s3-file-storage.serv...s3-file-storage.serv...health.controller.tshealth.controller.tshealth.controller.tsgql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.service.tsgithub.service.tsgithub.service.tsapi.config.tsapi.config.tsapi.config.tsdigital-oce...digital-oce...digital-oce...github.c...github.c...github.c...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tstailwind.config.tstailwind.config.tstailwind.config.tsuser-profile-c...user-profile-c...user-profile-c...user-profil...user-profil...user-profil...user-langs.tsuser-langs.tsuser-langs.tsrepo-icon-...repo-icon-...repo-icon-....tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardsharedshareds3-file-storage....s3-file-storage....s3-file-storage....health.controlle...health.controlle...health.controlle...gql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.servic...github.servic...github.servic...api.config.tsapi.config.tsapi.config.tsdigital-oc...digital-oc...digital-oc...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.control...user-card.control...user-card.control...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profil...user-profil...user-profil...highlight-card.servi...highlight-card.servi...highlight-card.servi...highligh...highligh...highligh...card-footer.tscard-footer.tscard-footer.tsuser-repo...user-repo...user-repo...user-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....tseach dot sized by file size \ No newline at end of file From b6abefaa364c5c7c3f97e52990808ad6687e989c Mon Sep 17 00:00:00 2001 From: NightKnight Date: Thu, 4 May 2023 14:49:32 +0300 Subject: [PATCH 07/12] feat: higlight card upload & storage (#39) * feat: higlight card upload & storage * feat: added parse int pipe * parseIntPipe * type fixes * feat: check if highlight needs regeneration * lint --- .../s3-file-storage.service.ts | 21 ++++++ .../highlight-card.controller.ts | 71 +++++++++++++++++++ .../highlight-card/highlight-card.module.ts | 2 + .../highlight-card/highlight-card.service.ts | 64 ++++++++++++++++- .../user-card/user-card.service.ts | 8 +-- typings/RequiresUpdateMeta.ts | 7 ++ 6 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 src/social-card/highlight-card/highlight-card.controller.ts create mode 100644 typings/RequiresUpdateMeta.ts diff --git a/src/s3-file-storage/s3-file-storage.service.ts b/src/s3-file-storage/s3-file-storage.service.ts index 7bb47ec..6ee89ad 100644 --- a/src/s3-file-storage/s3-file-storage.service.ts +++ b/src/s3-file-storage/s3-file-storage.service.ts @@ -71,6 +71,27 @@ export class S3FileStorageService { } } + async getFileMeta (hash: string): Promise | null> { + try { + const response = await this.s3Client.send( + new HeadObjectCommand({ + Bucket: this.config.bucketName, + Key: hash, + }), + ); + + return response.Metadata ?? null; + } catch (error) { + if (error instanceof Error) { + if (error.name === "NotFound") { + return null; + } + } + + throw error; + } + } + async uploadFile ( fileContent: Buffer | Readable, hash: string, diff --git a/src/social-card/highlight-card/highlight-card.controller.ts b/src/social-card/highlight-card/highlight-card.controller.ts new file mode 100644 index 0000000..8cc2a6b --- /dev/null +++ b/src/social-card/highlight-card/highlight-card.controller.ts @@ -0,0 +1,71 @@ +import { Controller, Get, Header, HttpStatus, Param, ParseIntPipe, Redirect, Res, StreamableFile } from "@nestjs/common"; +import { + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, ApiResponse, + ApiTags, +} from "@nestjs/swagger"; +import { FastifyReply } from "fastify"; +import { HighlightCardService } from "./highlight-card.service"; + + +@Controller("highlights") +@ApiTags("Highlight social cards") +export class HighlightCardController { + constructor ( + private readonly highlightCardService: HighlightCardService, + ) {} + + @Get("/:id") + @ApiOperation({ + operationId: "generateHighlightSocialCard", + summary: "Gets latest cache aware social card link for :id or generates a new one", + }) + @Header("Content-Type", "image/png") + @ApiOkResponse({ type: StreamableFile, description: "Social card image" }) + @ApiNotFoundResponse({ description: "Highlight not found" }) + @ApiForbiddenResponse({ description: "Rate limit exceeded" }) + @ApiBadRequestResponse({ description: "Invalid highlight id" }) + @Redirect() + async generateHighlightSocialCard ( + @Param("id", ParseIntPipe) id: number, + @Res({ passthrough: true }) res: FastifyReply, + ): Promise { + const { fileUrl, hasFile, needsUpdate } = await this.highlightCardService.checkRequiresUpdate(id); + + if (hasFile && !needsUpdate) { + return res.status(HttpStatus.FOUND).redirect(fileUrl); + } + + const url = await this.highlightCardService.getHighlightCard(id); + + return res.status(HttpStatus.FOUND).redirect(url); + } + + @Get("/:id/metadata") + @ApiOperation({ + operationId: "getHighlightSocialCardMetadata", + summary: "Gets latest cache aware social card metadata for :id", + }) + @ApiNoContentResponse({ description: "Highlight social card image is up to date", status: HttpStatus.NO_CONTENT }) + @ApiResponse({ description: "Highlight social card image needs regeneration", status: HttpStatus.NOT_MODIFIED }) + @ApiNotFoundResponse({ description: "Highlight social card image not found", status: HttpStatus.NOT_FOUND }) + @ApiBadRequestResponse({ description: "Invalid highlight id", status: HttpStatus.BAD_REQUEST }) + async checkHighlightSocialCard ( + @Param("id", ParseIntPipe) id: number, + @Res({ passthrough: true }) res: FastifyReply, + ): Promise { + const { fileUrl, hasFile, needsUpdate, lastModified } = await this.highlightCardService.checkRequiresUpdate(id); + + return res + .headers({ + "x-amz-meta-last-modified": lastModified?.toISOString() ?? "", + "x-amz-meta-location": fileUrl, + }) + .status(hasFile ? needsUpdate ? HttpStatus.NOT_MODIFIED : HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND) + .send(); + } +} diff --git a/src/social-card/highlight-card/highlight-card.module.ts b/src/social-card/highlight-card/highlight-card.module.ts index 6eb0d70..6d42e1a 100644 --- a/src/social-card/highlight-card/highlight-card.module.ts +++ b/src/social-card/highlight-card/highlight-card.module.ts @@ -3,9 +3,11 @@ import { HttpModule } from "@nestjs/axios"; import { GithubModule } from "../../github/github.module"; import { S3FileStorageModule } from "../../s3-file-storage/s3-file-storage.module"; import { HighlightCardService } from "../highlight-card/highlight-card.service"; +import { HighlightCardController } from "./highlight-card.controller"; @Module({ imports: [HttpModule, GithubModule, S3FileStorageModule], providers: [HighlightCardService], + controllers: [HighlightCardController], }) export class HighlightCardModule {} diff --git a/src/social-card/highlight-card/highlight-card.service.ts b/src/social-card/highlight-card/highlight-card.service.ts index 3c9c586..ed39e13 100644 --- a/src/social-card/highlight-card/highlight-card.service.ts +++ b/src/social-card/highlight-card/highlight-card.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from "@nestjs/common"; +import { ForbiddenException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { HttpService } from "@nestjs/axios"; import { Resvg } from "@resvg/resvg-js"; import { Repository, Language } from "@octokit/graphql-schema"; @@ -11,6 +11,7 @@ import userProfileRepos from "../templates/shared/user-repos"; import tailwindConfig from "../templates/tailwind.config"; import { firstValueFrom } from "rxjs"; import highlightCardTemplate from "../templates/highlight-card.template"; +import RequiresUpdateMeta from "../../../typings/RequiresUpdateMeta"; interface HighlightCardData { title: string, @@ -22,6 +23,7 @@ interface HighlightCardData { langs: (Language & { size: number, })[], + updated_at: Date } @Injectable() @@ -42,7 +44,7 @@ export class HighlightCardService { const today30daysAgo = new Date((new Date).setDate(today.getDate() - 30)); const highlightReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/user/highlights/${highlightId}`)); - const { login, title, highlight: body } = highlightReq.data; + const { login, title, highlight: body, updated_at } = highlightReq.data; const reactionsReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/highlights/${highlightId}/reactions`)); const reactions = reactionsReq.data.reduce( (acc, curr) => acc + Number(curr.reaction_count), 0); @@ -76,6 +78,7 @@ export class HighlightCardService { langs: Array.from(Object.values(langs)).sort((a, b) => b.size - a.size), langTotal, repos: user.topRepositories.nodes?.filter(repo => !repo?.isPrivate && repo?.owner.login !== login) as Repository[], + updated_at: new Date(updated_at), }; } @@ -110,4 +113,61 @@ export class HighlightCardService { return { png: pngData.asPng(), svg }; } + + async checkRequiresUpdate (id: number): Promise { + const hash = `highlights/${String(id)}.png`; + const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`; + const hasFile = await this.s3FileStorageService.fileExists(hash); + + const returnVal: RequiresUpdateMeta = { + fileUrl, + hasFile, + needsUpdate: true, + lastModified: null, + }; + + if (hasFile) { + const lastModified = await this.s3FileStorageService.getFileLastModified(hash); + + returnVal.lastModified = lastModified; + + const { updated_at, reactions } = await this.getHighlightData(id); + const metadata = await this.s3FileStorageService.getFileMeta(hash); + const savedReactions = metadata?.["reactions-count"] ?? "0"; + + if (lastModified && lastModified > updated_at && savedReactions === String(reactions)) { + this.logger.debug(`Highlight ${id} exists in S3 with lastModified: ${lastModified.toISOString()} newer than updated_at: ${updated_at.toISOString()}, and reaction count is the same, redirecting to ${fileUrl}`); + returnVal.needsUpdate = false; + } + } + + return returnVal; + } + + async getHighlightCard (id: number): Promise { + const { remaining } = await this.githubService.rateLimit(); + + if (remaining < 1000) { + throw new ForbiddenException("Rate limit exceeded"); + } + + const highlightData = await this.getHighlightData(id); + + try { + const hash = `highlights/${String(id)}.png`; + const fileUrl = `${this.s3FileStorageService.getCdnEndpoint()}${hash}`; + + const { png } = await this.generateCardBuffer(id, highlightData); + + await this.s3FileStorageService.uploadFile(png, hash, "image/png", { "reactions-count": String(highlightData.reactions) }); + + this.logger.debug(`Highlight ${id} did not exist in S3, generated image and uploaded to S3, redirecting`); + + return fileUrl; + } catch (e) { + this.logger.error(`Error generating highlight card for ${id}`, e); + + throw (new NotFoundException); + } + } } diff --git a/src/social-card/user-card/user-card.service.ts b/src/social-card/user-card/user-card.service.ts index b64edb3..c730a76 100644 --- a/src/social-card/user-card/user-card.service.ts +++ b/src/social-card/user-card/user-card.service.ts @@ -11,13 +11,7 @@ import userLangs from "../templates/shared/user-langs"; import userProfileRepos from "../templates/shared/user-repos"; import userProfileCardTemplate from "../templates/user-profile-card.template"; import tailwindConfig from "../templates/tailwind.config"; - -interface RequiresUpdateMeta { - fileUrl: string, - hasFile: boolean; - needsUpdate: boolean; - lastModified: Date | null, -} +import RequiresUpdateMeta from "../../../typings/RequiresUpdateMeta"; interface UserCardData { id: User["databaseId"], diff --git a/typings/RequiresUpdateMeta.ts b/typings/RequiresUpdateMeta.ts new file mode 100644 index 0000000..1165248 --- /dev/null +++ b/typings/RequiresUpdateMeta.ts @@ -0,0 +1,7 @@ + +export default interface RequiresUpdateMeta { + fileUrl: string, + hasFile: boolean; + needsUpdate: boolean; + lastModified: Date | null, +} From f089971d2017fa1cb4b03ca6df7a2d8dda7f5e0d Mon Sep 17 00:00:00 2001 From: NightKnight Date: Thu, 4 May 2023 11:53:20 +0000 Subject: [PATCH 08/12] chore(minor): release 2.1.0-beta.4 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.4](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.3...v2.1.0-beta.4) (2023-05-04) ### 🍕 Features * higlight card upload & storage ([#39](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/39)) ([b6abefa](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/b6abefaa364c5c7c3f97e52990808ad6687e989c)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adf5c2c..7b151bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.4](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.3...v2.1.0-beta.4) (2023-05-04) + + +### 🍕 Features + +* higlight card upload & storage ([#39](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/39)) ([b6abefa](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/b6abefaa364c5c7c3f97e52990808ad6687e989c)) + ## [2.1.0-beta.3](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.2...v2.1.0-beta.3) (2023-05-02) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d8db9cb..bc48da3 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.3", + "version": "2.1.0-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.3", + "version": "2.1.0-beta.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ea3d3fb..1af25a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.3", + "version": "2.1.0-beta.4", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index 3f9f7cd..09f9c4d 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardsharedshareds3-file-storage....s3-file-storage....s3-file-storage....health.controlle...health.controlle...health.controlle...gql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.servic...github.servic...github.servic...api.config.tsapi.config.tsapi.config.tsdigital-oc...digital-oc...digital-oc...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.control...user-card.control...user-card.control...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profil...user-profil...user-profil...highlight-card.servi...highlight-card.servi...highlight-card.servi...highligh...highligh...highligh...card-footer.tscard-footer.tscard-footer.tsuser-repo...user-repo...user-repo...user-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardsharedshareds3-file-storage.s...s3-file-storage.s...s3-file-storage.s...health.controll...health.controll...health.controll...gql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.servi...github.servi...github.servi...api.config.tsapi.config.tsapi.config.tsdigital-o...digital-o...digital-o...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.contro...user-card.contro...user-card.contro...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profi...user-profi...user-profi...highlight-card.service.tshighlight-card.service.tshighlight-card.service.tshighlight-card.c...highlight-card.c...highlight-card.c...highlight...highlight...highlight...card-footer.tscard-footer.tscard-footer.tsuser-repo...user-repo...user-repo...user-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....tseach dot sized by file size \ No newline at end of file From 1555a25a114b0d812c058d1a9728706a63965b44 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Fri, 5 May 2023 17:29:56 +0300 Subject: [PATCH 09/12] feat: highlights getting repo name & languages from pr's repo (#41) * feat: github pr data insertion * requested changes * nit: remove unneeded typing * lint * type refractor * lint --- src/github/github.service.ts | 11 +++- src/github/gql/get-repo.ts | 37 +++++++++++++ .../highlight-card/highlight-card.service.ts | 55 ++++++++----------- .../templates/shared/user-repos.ts | 3 +- 4 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 src/github/gql/get-repo.ts diff --git a/src/github/github.service.ts b/src/github/github.service.ts index 88087e7..ed15f36 100644 --- a/src/github/github.service.ts +++ b/src/github/github.service.ts @@ -1,9 +1,10 @@ import { Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigType } from "@nestjs/config"; import { graphql } from "@octokit/graphql"; -import { RateLimit, User } from "@octokit/graphql-schema"; +import { RateLimit, Repository, User } from "@octokit/graphql-schema"; import GithubConfig from "../config/github.config"; +import getRepo from "./gql/get-repo"; import getUser from "./gql/get-user"; @Injectable() @@ -29,6 +30,14 @@ export class GithubService { return user; } + async getRepo (owner: string, repo: string) { + const { query, variables } = getRepo(owner, repo); + + const { repository } = await this.graphqlWithAuth<{ repository: Repository }>(query, variables); + + return repository; + } + async rateLimit () { const { rateLimit } = await this.graphqlWithAuth<{ rateLimit: RateLimit }>(`query { rateLimit { diff --git a/src/github/gql/get-repo.ts b/src/github/gql/get-repo.ts new file mode 100644 index 0000000..7c9a60c --- /dev/null +++ b/src/github/gql/get-repo.ts @@ -0,0 +1,37 @@ +const getRepo = (owner: string, repo: string) => ({ + query: ` +query ($owner: String!, $repo: String!) { + repository( + owner: $owner + name: $repo + ) { + id + name + databaseId + nameWithOwner + owner { + id + login + avatarUrl + } + languages (first: 100, orderBy: { field: SIZE, direction: DESC }) { + edges { + node { + id + color + name + } + size + } + totalSize + totalCount + } + } +}`, + variables: { + owner, + repo, + }, +}); + +export default getRepo; diff --git a/src/social-card/highlight-card/highlight-card.service.ts b/src/social-card/highlight-card/highlight-card.service.ts index ed39e13..e0f6667 100644 --- a/src/social-card/highlight-card/highlight-card.service.ts +++ b/src/social-card/highlight-card/highlight-card.service.ts @@ -18,12 +18,13 @@ interface HighlightCardData { body: string, reactions: number, avatarUrl: string, - repos: Repository[], + repo: Repository, langTotal: number, langs: (Language & { size: number, })[], - updated_at: Date + updated_at: Date, + url: string, } @Injectable() @@ -37,48 +38,36 @@ export class HighlightCardService { ) {} private async getHighlightData (highlightId: number): Promise { - const langs: Record = {}; - const today = (new Date); - const today30daysAgo = new Date((new Date).setDate(today.getDate() - 30)); - const highlightReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/user/highlights/${highlightId}`)); - const { login, title, highlight: body, updated_at } = highlightReq.data; + const { login, title, highlight: body, updated_at, url } = highlightReq.data; const reactionsReq = await firstValueFrom(this.httpService.get(`https://api.opensauced.pizza/v1/highlights/${highlightId}/reactions`)); const reactions = reactionsReq.data.reduce( (acc, curr) => acc + Number(curr.reaction_count), 0); + const [owner, repoName] = url.replace("https://github.com/", "").split("/"); + const user = await this.githubService.getUser(login); - const langRepos = user.repositories.nodes?.filter(repo => new Date(String(repo?.pushedAt)) > today30daysAgo) as Repository[]; - let langTotal = 0; - - langRepos.forEach(repo => { - repo.languages?.edges?.forEach(edge => { - if (edge?.node.id) { - langTotal += edge.size; - - if (!Object.keys(langs).includes(edge.node.id)) { - langs[edge.node.id] = { - ...edge.node, - size: edge.size, - }; - } else { - langs[edge.node.id].size += edge.size; - } - } - }); - }); + const repo = await this.githubService.getRepo(owner, repoName); + + const langList = repo.languages?.edges?.flatMap(edge => { + if (edge) { + return { + ...edge.node, + size: edge.size, + }; + } + }) as (Language & { size: number })[]; return { title, body, reactions, avatarUrl: `${String(user.avatarUrl)}&size=150`, - langs: Array.from(Object.values(langs)).sort((a, b) => b.size - a.size), - langTotal, - repos: user.topRepositories.nodes?.filter(repo => !repo?.isPrivate && repo?.owner.login !== login) as Repository[], + langs: langList, + langTotal: repo.languages?.totalSize ?? 0, + repo, updated_at: new Date(updated_at), + url, }; } @@ -87,9 +76,9 @@ export class HighlightCardService { const { html } = await import("satori-html"); const satori = (await import("satori")).default; - const { title, body, reactions, avatarUrl, repos, langs, langTotal } = highlightData ? highlightData : await this.getHighlightData(highlightId); + const { title, body, reactions, avatarUrl, repo, langs, langTotal } = highlightData ? highlightData : await this.getHighlightData(highlightId); - const template = html(highlightCardTemplate(avatarUrl, title, body, userLangs(langs, langTotal), userProfileRepos(repos, 2), reactions)); + const template = html(highlightCardTemplate(avatarUrl, title, body, userLangs(langs, langTotal), userProfileRepos([repo], 2), reactions)); const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); diff --git a/src/social-card/templates/shared/user-repos.ts b/src/social-card/templates/shared/user-repos.ts index 4ab9ed2..faedf7d 100644 --- a/src/social-card/templates/shared/user-repos.ts +++ b/src/social-card/templates/shared/user-repos.ts @@ -2,8 +2,9 @@ import repoIconWithName from "./repo-icon-with-name"; import { Repository } from "@octokit/graphql-schema"; const userProfileRepos = (repos: Repository[], limit: number): string => { + const charLimit = limit === 1 ? 60 : repos.length === 1 ? 60 : 15; const repoList = repos.map(({ name, owner: { avatarUrl } }) => - repoIconWithName(`${name.substring(0, 15).replace(/\.+$/, "")}${name.length > 15 ? "..." : ""}`, `${String(avatarUrl)}&size=40`)); + repoIconWithName(`${name.substring(0, charLimit).replace(/\.+$/, "")}${name.length > charLimit ? "..." : ""}`, `${String(avatarUrl)}&size=40`)); return `${repoList.slice(0, limit).join("")}${repoList.length > limit ? `

+${repoList.length - limit}

` From 0e4dfac3831bb2ba03454a6f8ec07b77210e06f1 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Fri, 5 May 2023 14:33:38 +0000 Subject: [PATCH 10/12] chore(minor): release 2.1.0-beta.5 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.5](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.4...v2.1.0-beta.5) (2023-05-05) ### 🍕 Features * highlights getting repo name & languages from pr's repo ([#41](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/41)) ([1555a25](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/1555a25a114b0d812c058d1a9728706a63965b44)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b151bc..cc61a90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.5](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.4...v2.1.0-beta.5) (2023-05-05) + + +### 🍕 Features + +* highlights getting repo name & languages from pr's repo ([#41](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/41)) ([1555a25](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/1555a25a114b0d812c058d1a9728706a63965b44)) + ## [2.1.0-beta.4](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.3...v2.1.0-beta.4) (2023-05-04) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index bc48da3..9271535 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.4", + "version": "2.1.0-beta.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.4", + "version": "2.1.0-beta.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1af25a5..66e2991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.4", + "version": "2.1.0-beta.5", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index 09f9c4d..f286b6d 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardsharedshareds3-file-storage.s...s3-file-storage.s...s3-file-storage.s...health.controll...health.controll...health.controll...gql/get-user.tsgql/get-user.tsgql/get-user.tsgithub.servi...github.servi...github.servi...api.config.tsapi.config.tsapi.config.tsdigital-o...digital-o...digital-o...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.contro...user-card.contro...user-card.contro...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profi...user-profi...user-profi...highlight-card.service.tshighlight-card.service.tshighlight-card.service.tshighlight-card.c...highlight-card.c...highlight-card.c...highlight...highlight...highlight...card-footer.tscard-footer.tscard-footer.tsuser-repo...user-repo...user-repo...user-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardgqlgqlsharedshareds3-file-storage.s...s3-file-storage.s...s3-file-storage.s...health.controll...health.controll...health.controll...github.servic...github.servic...github.servic...api.config.tsapi.config.tsapi.config.tsdigital-o...digital-o...digital-o...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.contro...user-card.contro...user-card.contro...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profi...user-profi...user-profi...highlight-card.service.tshighlight-card.service.tshighlight-card.service.tshighlight-card.c...highlight-card.c...highlight-card.c...highlight...highlight...highlight...get-user.tsget-user.tsget-user.tsget-repo.tsget-repo.tsget-repo.tscard-footer.tscard-footer.tscard-footer.tsuser-repos.tsuser-repos.tsuser-repos.tsuser-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....gql.tseach dot sized by file size \ No newline at end of file From acbe81a39303d7495fd4bebd51306bd3fe8d9d18 Mon Sep 17 00:00:00 2001 From: NightKnight Date: Tue, 9 May 2023 22:49:58 +0300 Subject: [PATCH 11/12] feat: UI adjustments (#42) * feat: remove lang gaps * feat: decrease user repos limit * feat: use user's github login name --- src/social-card/templates/shared/card-footer.ts | 2 +- src/social-card/user-card/user-card.service.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/social-card/templates/shared/card-footer.ts b/src/social-card/templates/shared/card-footer.ts index 6ea08ec..caa6fcd 100644 --- a/src/social-card/templates/shared/card-footer.ts +++ b/src/social-card/templates/shared/card-footer.ts @@ -21,7 +21,7 @@ const cardFooter = (langs: string, repos: string, reactions?: number) => `
-
+
${langs}
diff --git a/src/social-card/user-card/user-card.service.ts b/src/social-card/user-card/user-card.service.ts index c730a76..7d93ea0 100644 --- a/src/social-card/user-card/user-card.service.ts +++ b/src/social-card/user-card/user-card.service.ts @@ -22,6 +22,7 @@ interface UserCardData { langTotal: number, repos: Repository[], avatarUrl: string, + formattedName: string, } @@ -69,6 +70,7 @@ export class UserCardService { langTotal, repos: user.topRepositories.nodes?.filter(repo => !repo?.isPrivate && repo?.owner.login !== username) as Repository[], avatarUrl: `${String(user.avatarUrl)}&size=150`, + formattedName: user.login ?? username, }; } @@ -77,9 +79,9 @@ export class UserCardService { const { html } = await import("satori-html"); const satori = (await import("satori")).default; - const { avatarUrl, repos, langs, langTotal } = userData ? userData : await this.getUserData(username); + const { avatarUrl, repos, langs, langTotal, formattedName } = userData ? userData : await this.getUserData(username); - const template = html(userProfileCardTemplate(avatarUrl, username, userLangs(langs, langTotal), userProfileRepos(repos, 4))); + const template = html(userProfileCardTemplate(avatarUrl, formattedName, userLangs(langs, langTotal), userProfileRepos(repos, 3))); const interArrayBuffer = await fs.readFile("node_modules/@fontsource/inter/files/inter-all-400-normal.woff"); From f0b854a17aa04c91e41bdd9c9c0d1ff15a76df4a Mon Sep 17 00:00:00 2001 From: NightKnight Date: Tue, 9 May 2023 19:53:57 +0000 Subject: [PATCH 12/12] chore(minor): release 2.1.0-beta.6 on beta channel [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.1.0-beta.6](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.5...v2.1.0-beta.6) (2023-05-09) ### 🍕 Features * UI adjustments ([#42](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/42)) ([acbe81a](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/acbe81a39303d7495fd4bebd51306bd3fe8d9d18)) --- CHANGELOG.md | 7 +++++++ npm-shrinkwrap.json | 4 ++-- package.json | 2 +- public/diagram.svg | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc61a90..7f9b553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ > All notable changes to this project will be documented in this file +## [2.1.0-beta.6](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.5...v2.1.0-beta.6) (2023-05-09) + + +### 🍕 Features + +* UI adjustments ([#42](https://github.com/open-sauced/opengraph.opensauced.pizza/issues/42)) ([acbe81a](https://github.com/open-sauced/opengraph.opensauced.pizza/commit/acbe81a39303d7495fd4bebd51306bd3fe8d9d18)) + ## [2.1.0-beta.5](https://github.com/open-sauced/opengraph.opensauced.pizza/compare/v2.1.0-beta.4...v2.1.0-beta.5) (2023-05-05) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9271535..8b7f894 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.5", + "version": "2.1.0-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.5", + "version": "2.1.0-beta.6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 66e2991..f9e53b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@open-sauced/opengraph.opensauced.pizza", - "version": "2.1.0-beta.5", + "version": "2.1.0-beta.6", "keywords": [], "description": "OpenGraph dot Open Sauced is a general purpose social card generator", "author": "Ahmed Mohamed Atwa ", diff --git a/public/diagram.svg b/public/diagram.svg index f286b6d..201a8d0 100644 --- a/public/diagram.svg +++ b/public/diagram.svg @@ -1 +1 @@ -social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardgqlgqlsharedshareds3-file-storage.s...s3-file-storage.s...s3-file-storage.s...health.controll...health.controll...health.controll...github.servic...github.servic...github.servic...api.config.tsapi.config.tsapi.config.tsdigital-o...digital-o...digital-o...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.contro...user-card.contro...user-card.contro...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profi...user-profi...user-profi...highlight-card.service.tshighlight-card.service.tshighlight-card.service.tshighlight-card.c...highlight-card.c...highlight-card.c...highlight...highlight...highlight...get-user.tsget-user.tsget-user.tsget-repo.tsget-repo.tsget-repo.tscard-footer.tscard-footer.tscard-footer.tsuser-repos.tsuser-repos.tsuser-repos.tsuser-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....gql.tseach dot sized by file size \ No newline at end of file +social-cardsocial-cards3-file-storages3-file-storagehealthhealthgithubgithubconfigconfiguser-carduser-cardtemplatestemplateshighlight-cardhighlight-cardgqlgqlsharedshareds3-file-storage.s...s3-file-storage.s...s3-file-storage.s...health.controll...health.controll...health.controll...github.servic...github.servic...github.servic...api.config.tsapi.config.tsapi.config.tsdigital-o...digital-o...digital-o...main.tsmain.tsmain.tsapp.module.tsapp.module.tsapp.module.tsuser-card.service.tsuser-card.service.tsuser-card.service.tsuser-card.contro...user-card.contro...user-card.contro...user-card...user-card...user-card...tailwind.config.tstailwind.config.tstailwind.config.tshighlight-ca...highlight-ca...highlight-ca...user-profi...user-profi...user-profi...highlight-card.service.tshighlight-card.service.tshighlight-card.service.tshighlight-card.c...highlight-card.c...highlight-card.c...highlight...highlight...highlight...get-user.tsget-user.tsget-user.tsget-repo.tsget-repo.tsget-repo.tscard-footer.tscard-footer.tscard-footer.tsuser-repos.tsuser-repos.tsuser-repos.tsuser-lang...user-lang...user-lang...repo-ico...repo-ico...repo-ico....gql.tseach dot sized by file size \ No newline at end of file