├── .gitignore ├── LICENSE ├── README.md ├── api └── index.ts ├── badge.ts ├── badges └── index.ts ├── hacker-news.ts ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Anand Chowdhary 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📛 HackerBadge 2 | 3 | Embeddable Hacker News badges for your post/launch. A serverless function written in TypeScript and deployed on ZEIT. 4 | 5 | [![Light badge example](https://hackerbadge.now.sh/api?id=8863)](https://hackerbadge.now.sh/api?id=8863) 6 | 7 | [![Orange badge example](https://hackerbadge.now.sh/api?id=8863&type=orange)](https://hackerbadge.now.sh/api?id=8863&type=orange) 8 | 9 | [![Dark badge example](https://hackerbadge.now.sh/api?id=8863&type=dark)](https://hackerbadge.now.sh/api?id=8863&type=dark) 10 | 11 | ## ⭐ Usage 12 | 13 | ### URLs 14 | 15 | - Light badge: https://hackerbadge.now.sh/api?id=8863 16 | - Orange badge: https://hackerbadge.now.sh/api?id=8863&type=orange 17 | - Dark badge: https://hackerbadge.now.sh/api?id=8863&type=dark 18 | 19 | ### HTML 20 | 21 | ```html 22 | 23 | Featured on Hacker News 27 | 28 | ``` 29 | 30 | ### Markdown 31 | 32 | ```md 33 | [![Featured on Hacker News](https://hackerbadge.now.sh/api?id=8863)](https://news.ycombinator.com/item?id=8863) 34 | ``` 35 | 36 | ### I18N 37 | 38 | [![Hindi badge example](https://hackerbadge.now.sh/api?id=8863&hackerNews=हैकर%20न्यूज़&featuredOn=प्रदर्शित)](https://hackerbadge.now.sh/api?id=8863&hackerNews=हैकर%20न्यूज़&featuredOn=प्रदर्शित) 39 | 40 | https://hackerbadge.now.sh/api?id=8863&hackerNews=हैकर%20न्यूज़&featuredOn=प्रदर्शित 41 | 42 | ## 🔨 Development 43 | 44 | [![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/import/project?template=https://github.com/AnandChowdhary/hackerbadge) 45 | 46 | ## 📄 License 47 | 48 | [MIT](./LICENSE) © [Anand Chowdhary](https://anandchowdhary.com) 49 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from "@now/node"; 2 | import { getBadge } from "../badge"; 3 | 4 | export default async (req: NowRequest, res: NowResponse) => { 5 | try { 6 | if (typeof req.query.id !== "string") 7 | throw new Error("ID should be provided"); 8 | const id = parseInt(req.query.id); 9 | const type = 10 | typeof req.query.type === "string" ? req.query.type : undefined; 11 | const font = 12 | typeof req.query.font === "string" ? req.query.font : undefined; 13 | const hackerNews = 14 | typeof req.query.hackerNews === "string" 15 | ? req.query.hackerNews 16 | : undefined; 17 | const featuredOn = 18 | typeof req.query.featuredOn === "string" 19 | ? req.query.featuredOn 20 | : undefined; 21 | const badge = await getBadge(id, type, font, hackerNews, featuredOn); 22 | res.setHeader("Content-Type", "image/svg+xml"); 23 | res.setHeader("Cache-Control", "max-age=86400"); 24 | return res.send(badge); 25 | } catch (error) { 26 | res.status(400); 27 | res.json({ error }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /badge.ts: -------------------------------------------------------------------------------- 1 | import { LIGHT, ORANGE, DARK } from "./badges"; 2 | import { getNumberOfUpvotes } from "./hacker-news"; 3 | import width from "string-pixel-width"; 4 | 5 | export const getBadge = async ( 6 | id: number, 7 | type = "light", 8 | font = "Arial", 9 | hackerNews = "Hacker News", 10 | featuredOn = "FEATURED ON" 11 | ) => { 12 | let badge = LIGHT; 13 | if (type === "orange") badge = ORANGE; 14 | if (type === "dark") badge = DARK; 15 | const upvotes = await getNumberOfUpvotes(id); 16 | const textWidth = width(hackerNews, { size: 25 }); 17 | const numberWidth = width(upvotes.toString(), { size: 18 }); 18 | return badge 19 | .replace(/{WIDTH}/g, (110 + numberWidth + textWidth).toString()) 20 | .replace(/{SHORT_WIDTH}/g, (110 + numberWidth + textWidth - 2).toString()) 21 | .replace(/{NUM_POS}/g, (textWidth + 90).toString()) 22 | .replace( 23 | /{TRIANGLE_POS}/g, 24 | (textWidth + 90 + (numberWidth - 19) / 2).toString() 25 | ) 26 | .replace(/{FONT}/g, font) 27 | .replace(/{HACKER_NEWS}/g, hackerNews) 28 | .replace(/{UPVOTES}/g, upvotes.toString()) 29 | .replace(/{FEATURED_ON}/g, featuredOn); 30 | }; 31 | -------------------------------------------------------------------------------- /badges/index.ts: -------------------------------------------------------------------------------- 1 | export const LIGHT = `{HACKER_NEWS}{UPVOTES}{FEATURED_ON}`; 2 | 3 | export const DARK = `{HACKER_NEWS}{UPVOTES}{FEATURED_ON}`; 4 | 5 | export const ORANGE = `{HACKER_NEWS}{UPVOTES}{FEATURED_ON}`; 6 | -------------------------------------------------------------------------------- /hacker-news.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getNumberOfUpvotes = async (id: number) => { 4 | const apiResponse = await axios.get<{ 5 | by: string; 6 | descendants: number; 7 | id: number; 8 | kids: number[]; 9 | score: number; 10 | time: number; 11 | title: string; 12 | type: "story" | "comment"; 13 | url: string; 14 | }>(`https://hacker-news.firebaseio.com/v0/item/${id}.json`); 15 | return apiResponse.data.score; 16 | }; 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badges", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@now/node": { 8 | "version": "1.4.1", 9 | "resolved": "https://registry.npmjs.org/@now/node/-/node-1.4.1.tgz", 10 | "integrity": "sha512-EjP/pdBMKsEMCGQ1OLLmBGnjA3QZG1erYTrMqmDVqypeQsY1UUFTY4h1C4d6WNq33qk/nMxpcJzuAhxt+nLQyg==", 11 | "dev": true, 12 | "requires": { 13 | "@types/node": "*" 14 | } 15 | }, 16 | "@types/axios": { 17 | "version": "0.14.0", 18 | "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", 19 | "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", 20 | "dev": true, 21 | "requires": { 22 | "axios": "*" 23 | } 24 | }, 25 | "@types/node": { 26 | "version": "13.7.5", 27 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.5.tgz", 28 | "integrity": "sha512-PfSBCTQhAQg6QBP4UhXgrZ/wQ3pjfwBr4sA7Aul+pC9XwGgm9ezrJF7OiC/I4Kf+7VPu/5ThKngAruqxyctZfA==", 29 | "dev": true 30 | }, 31 | "@types/string-pixel-width": { 32 | "version": "1.7.0", 33 | "resolved": "https://registry.npmjs.org/@types/string-pixel-width/-/string-pixel-width-1.7.0.tgz", 34 | "integrity": "sha512-Q/Gs9iOe7Ch9aXkNXj1zGmmR7oZ00cxq1UAYkj/VC9dv+FQd3oqCkS9t+SgYVxfLRkN4vq8Zfj46m2szc9uB/g==", 35 | "dev": true 36 | }, 37 | "axios": { 38 | "version": "0.21.1", 39 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 40 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 41 | "requires": { 42 | "follow-redirects": "^1.10.0" 43 | } 44 | }, 45 | "follow-redirects": { 46 | "version": "1.13.1", 47 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", 48 | "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" 49 | }, 50 | "lodash.deburr": { 51 | "version": "4.1.0", 52 | "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-4.1.0.tgz", 53 | "integrity": "sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s=" 54 | }, 55 | "string-pixel-width": { 56 | "version": "1.10.0", 57 | "resolved": "https://registry.npmjs.org/string-pixel-width/-/string-pixel-width-1.10.0.tgz", 58 | "integrity": "sha512-cOMpkH+CpxWAnrPsWUvPWhZxh25CzUukweT+6WF+Kwx6+G2ksg8flvELZusLyWiZzfFCjj1+QRRGwcPWZlwVYA==", 59 | "requires": { 60 | "lodash.deburr": "^4.1.0" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "badges", 3 | "version": "1.0.0", 4 | "repository": "git@github.com:AnandChowdhary/hackerbadge.git", 5 | "author": "Anand Chowdhary ", 6 | "license": "MIT", 7 | "scripts": { 8 | "local": "now dev" 9 | }, 10 | "devDependencies": { 11 | "@now/node": "^1.2.0", 12 | "@types/axios": "^0.14.0", 13 | "@types/string-pixel-width": "^1.7.0" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.21.1", 17 | "string-pixel-width": "^1.10.0" 18 | } 19 | } 20 | --------------------------------------------------------------------------------