選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

sponsors.ts 2.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import type { NextApiRequest, NextApiResponse } from 'next'
  2. const AV_SIZE = 32
  3. const PADDING = 4
  4. const COLS = 16
  5. type SponsorResult = { avatarUrl: string; login: string }
  6. type QueryResult = {
  7. node: { sponsorEntity: { avatarUrl: string; login: string } }
  8. }
  9. function getXY(i: number) {
  10. return [(i % COLS) * (AV_SIZE + PADDING), Math.floor(i / COLS) * (AV_SIZE + PADDING)]
  11. }
  12. export default async function GetSponsors(_req: NextApiRequest, res: NextApiResponse) {
  13. const sponsorInfo = await fetch('https://api.github.com/graphql', {
  14. method: 'POST',
  15. headers: {
  16. 'Content-Type': 'application/json',
  17. Authorization: 'bearer ' + process.env.GITHUB_API_SECRET,
  18. },
  19. body: JSON.stringify({
  20. query: `{
  21. viewer {
  22. sponsors(first: 0) {
  23. totalCount
  24. }
  25. sponsorshipsAsMaintainer(first: 100, orderBy: {
  26. field:CREATED_AT,
  27. direction:DESC
  28. }) {
  29. edges {
  30. node {
  31. sponsorEntity {
  32. ...on User {
  33. avatarUrl
  34. login
  35. }
  36. }
  37. }
  38. }
  39. }
  40. }
  41. }`,
  42. }),
  43. }).then((res) => res.json())
  44. // Get the total count of sponsors
  45. const totalCount: number = sponsorInfo.data.viewer.sponsors.totalCount
  46. // Map out the login and avatarUrl for each sponsor
  47. const sponsors = (
  48. sponsorInfo.data.viewer.sponsorshipsAsMaintainer.edges as QueryResult[]
  49. ).map<SponsorResult>((edge) => ({
  50. login: edge.node.sponsorEntity.login,
  51. avatarUrl: edge.node.sponsorEntity.avatarUrl?.replace(/&/g, '&amp;') ?? '',
  52. }))
  53. // If we're going to create a more link (see below), then make room for it if necessary
  54. if (totalCount > 100 && sponsors.length % COLS <= 2) {
  55. sponsors.pop()
  56. sponsors.pop()
  57. sponsors.pop()
  58. }
  59. // Generate images for each of the first 100 sponsors.
  60. const avatars = sponsors
  61. .map(({ avatarUrl, login }, i) => {
  62. const [x, y] = getXY(i)
  63. return `<image alt="${login}" href="${avatarUrl}" x="${x}" y="${y}" width="${AV_SIZE}" height="${AV_SIZE}"/>`
  64. })
  65. .join('')
  66. // If there are more than 100 sponsors, generate some text to list how many more.
  67. let more = ''
  68. if (totalCount > sponsors.length) {
  69. // More text
  70. const [x, y] = getXY(sponsors.length)
  71. const width = (AV_SIZE + PADDING) * 3
  72. more = `<g transform="translate(${x},${y})"><text text-lenth="${width}" font-family="Arial" font-size="12px" font-weight="bold" text-anchor="middle" text-align="center" x="${
  73. width / 2
  74. }" y="${AV_SIZE / 2 + 3}">...and ${totalCount - sponsors.length} more!</text></g>`
  75. }
  76. const svgImage = `
  77. <svg xmlns="http://www.w3.org/2000/svg"><a href="https://github.com/sponsors/steveruizok">${avatars}${more}</a></svg>`
  78. res
  79. .status(200)
  80. .setHeader('Cache-Control', 'max-age=604800')
  81. .setHeader('Content-Type', 'image/svg+xml')
  82. .send(svgImage)
  83. }